67 Commits

Author SHA1 Message Date
b684bf195e Merge pull request #1 from Underslumber/fix/update-translation
Fix/update translation
2026-04-11 00:02:05 +03:00
39ac7a54ce Обновлен перевод класса Illrigger 2026-04-11 00:01:05 +03:00
92c78f0813 Уточнены правила AGENTS.md по ссылкам, триггерам CI и approval gates 2026-04-10 22:53:40 +03:00
07c7fc8aa7 Поднята версия релиза до v0.2.6 2026-04-10 22:52:43 +03:00
cb37f422ac Merge remote-tracking branch 'origin/main' 2026-04-10 22:50:04 +03:00
72bab59520 Fix 2026-04-10 22:29:04 +03:00
1ca6d579da Слияние изменений из Gitea 2026-04-10 19:38:03 +03:00
6ede25dc35 Обновлена версия мода до v0.2.4 2026-04-10 15:26:10 +03:00
de85438afe Добавлен GitHub Actions для сборки и релизов 2026-04-10 14:53:04 +03:00
111cf8c269 Восстановлены потерянные правила при рефакторинге AGENTS.md 2026-04-10 12:37:18 +03:00
d2299199ba Исправлена кодировка meta.lsx и выпущен hotfix v0.2.5
All checks were successful
Build Mod Package / build (push) Successful in 4s
2026-04-10 12:23:12 +03:00
ec062817dc Поднята версия релиза до v0.2.4
All checks were successful
Build Mod Package / build (push) Successful in 11s
2026-04-10 12:14:31 +03:00
f56e10748e Уточнен регистр терминов отдыха 2026-04-10 12:06:47 +03:00
ac1bd03426 Обновить автора на Underslumber Team в meta.lsx и scripts/build.ps1 2026-04-10 01:47:13 +03:00
07f557ca30 Обновлено описание проекта в README 2026-04-09 21:41:42 +03:00
7f189aa741 Ужесточены сценарии обновления перевода для AI-агента 2026-04-09 16:03:02 +03:00
d048a33c55 Уточнил процесс обновления перевода в ACTIONS.md 2026-04-09 13:18:51 +03:00
4e99dfdd92 Объединил автоматизацию локального обновления перевода 2026-04-09 13:03:40 +03:00
5d54da9048 Автоматизировал локальное обновление перевода 2026-04-09 13:02:40 +03:00
d96db4e1f0 Уточнил правило для повторных pending-вопросов 2026-04-09 12:14:36 +03:00
c40712701c Исправлен XPath Version64 в правилах релиза AGENTS.md 2026-04-09 11:59:29 +03:00
c9595312ab Уточнены правила ссылок и переключения веток в AGENTS.md 2026-04-09 11:55:17 +03:00
91e12e4ba1 Повышена версия мода до v0.2.3 в ModuleInfo Version64
All checks were successful
Build Mod Package / build (push) Successful in 16s
2026-04-09 11:35:03 +03:00
921a5a3156 Обновлён перевод russian.xml и синхронизирована зависимость в meta.lsx 2026-04-09 11:32:32 +03:00
2ab32b258c Оптимизированы правила подтверждения действий 2026-04-09 11:23:49 +03:00
175c1dbaed Добавлено предложение обновления зависимости после обновления перевода 2026-04-09 11:16:26 +03:00
e9bfbfe74f Уточнены правила по веткам и предварительному показу небольших правок 2026-04-09 11:13:08 +03:00
6782cfcd87 Merge branch 'feat/sync-parent-meta' 2026-04-09 10:58:42 +03:00
eaf84ad605 Скрипт синхронизации parent meta переведен на загрузку из git 2026-04-09 10:58:18 +03:00
65e3f5b48e Добавлены action и скрипт синхронизации parent meta, уточнены правила подтверждения 2026-04-09 10:54:30 +03:00
9519b92771 Уточнил правило смены ветки: останавливать выполнение до явного решения пользователя 2026-04-09 10:37:19 +03:00
6257027a13 Обновлено описание мода в meta.lsx 2026-04-09 10:34:23 +03:00
cde9194ed5 Разделил правила AGENTS.md на общие и проектные 2026-04-09 08:58:43 +03:00
1c8cf13f67 Уточнен принцип сжатия правил: оптимизация под AI-агента 2026-04-09 08:53:27 +03:00
74a8942999 Уточнен формат ссылки на релиз: [version](url) 2026-04-09 08:51:14 +03:00
7f8f09a3ac Исправлен set-version: обновление только ModuleInfo.Version64 и сужены правила в AGENTS
All checks were successful
Build Mod Package / build (push) Successful in 9s
2026-04-09 08:47:39 +03:00
70f93c3d29 Обновлен перевод russian.xml по upstream: добавлены новые строки и синхронизирована Метка охотника
All checks were successful
Build Mod Package / build (push) Successful in 10s
2026-04-09 08:33:59 +03:00
4aa2e136b2 Уточнить поведение при отсутствии совпадения action 2026-04-09 08:26:40 +03:00
3ae30a5263 Добавить и оптимизировать внутренний ранбук ACTIONS.md 2026-04-09 08:15:43 +03:00
4da26911fe Уточнить политику веток: предлагать ветку в начале и MR/merge в конце 2026-04-09 07:53:33 +03:00
df1daee6ab Перенести комментарий о CI-особенности Divine в build.ps1 2026-04-09 07:49:11 +03:00
9d1a26c8e0 Сжать AGENTS.md и убрать дубли правил публикации 2026-04-09 07:46:53 +03:00
8a4970742c Упростить правила публикации и обновлять только ModuleInfo/Version64 2026-04-09 07:37:55 +03:00
b50a6a2f95 Зафиксировать изменения meta.lsx из BG3 Toolkit и правило релизного тега
All checks were successful
Build Mod Package / build (push) Successful in 2s
2026-04-09 07:29:24 +03:00
4646b51459 Подготовить версию v0.2.1 в meta.lsx
All checks were successful
Build Mod Package / build (push) Successful in 3s
2026-04-09 07:03:15 +03:00
a72b7bc1e1 Добавить скрипт обновления версии meta.lsx перед выпуском 2026-04-09 07:00:35 +03:00
7aca648396 Добавить правило про ссылки на архивы релизов
All checks were successful
Build Mod Package / build (push) Successful in 4s
2026-04-08 23:48:58 +03:00
36129b15d1 Дополнить русскую локализацию по оригинальному english.xml
All checks were successful
Build Mod Package / build (push) Successful in 4s
2026-04-08 23:43:31 +03:00
97ca95ba16 Исправлена кодировка описания мода
All checks were successful
Build Mod Package / build (push) Successful in 15s
2026-04-08 23:23:49 +03:00
ad129e15d5 chore: add metadata.lsf for DnD 5.5e AIO Russian mod GUI 2026-04-08 23:19:00 +03:00
c8371a3fec Обновлена локализация по глоссарию, дополнены черты происхождения и добавлен логотип публикации
All checks were successful
Build Mod Package / build (push) Successful in 55s
2026-04-08 22:20:13 +03:00
321fef2f63 Уточнены ориентиры для перевода в AGENTS.md 2026-04-08 22:06:59 +03:00
eb9ba5eefa Добавлен перевод новых строк локализации для голиафа и стрелка
All checks were successful
Build Mod Package / build (push) Successful in 30s
2026-04-08 21:52:36 +03:00
c0524832d0 Добавлен BOM для meta.lsx в сборке и уточнён перевод " Расщепление разума\
All checks were successful
Build Mod Package / build (push) Successful in 35s
2026-04-08 21:31:40 +03:00
e78610c702 Update Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
All checks were successful
Build Mod Package / build (push) Successful in 28s
2026-04-08 21:17:13 +03:00
5c0a44a71e Update Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
All checks were successful
Build Mod Package / build (push) Successful in 49s
2026-04-08 21:16:39 +03:00
c859ddc50c Update Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml 2026-04-08 21:16:20 +03:00
5f3ca9ae0d Update Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml 2026-04-08 21:14:33 +03:00
782879d73e Update Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
All checks were successful
Build Mod Package / build (push) Successful in 46s
2026-04-08 21:12:06 +03:00
d5cd0300da revert 85a7ec546c
revert Исправлен перевод CSV и обновлены правила коммита
2026-04-08 20:40:54 +03:00
85a7ec546c Исправлен перевод CSV и обновлены правила коммита 2026-04-08 20:37:02 +03:00
37d34f8406 Merge pull request 'fix: синхронизировать термины russian.xml с глоссарием' (#2) from fix/translation-glossary-sync-20260408 into main
All checks were successful
Build Mod Package / build (push) Successful in 8s
Reviewed-on: mikhail/bg3-dnd55e-russian-localization#2
2026-04-08 20:10:29 +03:00
fba22ac1aa Merge branch 'main' into fix/translation-glossary-sync-20260408 2026-04-08 20:10:19 +03:00
0d42f9ca34 fix: синхронизировать термины russian.xml с глоссарием 2026-04-08 20:06:26 +03:00
7a410d4124 Merge branch 'main' of https://git.shahovalov.ru/mikhail/bg3-dnd55e-russian-localization
All checks were successful
Build Mod Package / build (push) Successful in 23s
2026-04-08 19:55:46 +03:00
945ede2583 Исправлена работа Invoke-WebRequest на Windows PowerShell 2026-04-08 19:53:47 +03:00
469cd8bc37 docs: add glossary file reference to AGENTS.md 2026-04-08 19:53:10 +03:00
19 changed files with 2072 additions and 553 deletions

View File

@@ -59,7 +59,7 @@ jobs:
throw "Could not find a downloadable LSLib zip asset in the latest release." throw "Could not find a downloadable LSLib zip asset in the latest release."
} }
Invoke-WebRequest -Uri $asset.browser_download_url -OutFile ".tools/lslib/lslib.zip" Invoke-WebRequest -UseBasicParsing -Uri $asset.browser_download_url -OutFile ".tools/lslib/lslib.zip"
Expand-Archive -LiteralPath ".tools/lslib/lslib.zip" -DestinationPath ".tools/lslib" -Force Expand-Archive -LiteralPath ".tools/lslib/lslib.zip" -DestinationPath ".tools/lslib" -Force
- name: Build .pak - name: Build .pak
@@ -157,4 +157,4 @@ jobs:
Accept = "application/json" Accept = "application/json"
} }
Invoke-WebRequest -Method Post -Uri "$apiBase/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($assetName))" -Headers $uploadHeaders -ContentType "application/octet-stream" -InFile $zipPath Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$apiBase/releases/$($release.id)/assets?name=$([uri]::EscapeDataString($assetName))" -Headers $uploadHeaders -ContentType "application/octet-stream" -InFile $zipPath

117
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
name: Build Mod Package
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: write
jobs:
build:
runs-on: windows-latest
defaults:
run:
shell: pwsh
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Prepare workspace
run: |
$ErrorActionPreference = "Stop"
if (-not (Test-Path -LiteralPath "Mods\\DnD 5.5e AIO Russian\\Localization\\Russian\\russian.xml")) {
throw "Repository sources are not available in the runner workspace."
}
New-Item -ItemType Directory -Path ".tools\\lslib" -Force | Out-Null
New-Item -ItemType Directory -Path "build" -Force | Out-Null
- name: Resolve version tag
id: vars
run: |
$ErrorActionPreference = "Stop"
$versionTag = ""
if ($env:GITHUB_REF -like "refs/tags/v*") {
$versionTag = $env:GITHUB_REF_NAME
}
"version_tag=$versionTag" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Download latest LSLib release
run: |
$ErrorActionPreference = "Stop"
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/Norbyte/lslib/releases/latest"
$asset = $release.assets | Where-Object { $_.name -match '\.zip$' } | Select-Object -First 1
if (-not $asset) {
throw "Could not find a downloadable LSLib zip asset in the latest release."
}
Invoke-WebRequest -UseBasicParsing -Uri $asset.browser_download_url -OutFile ".tools/lslib/lslib.zip"
Expand-Archive -LiteralPath ".tools/lslib/lslib.zip" -DestinationPath ".tools/lslib" -Force
- name: Build .pak
run: |
$ErrorActionPreference = "Stop"
$divine = Get-ChildItem -Path ".tools\\lslib" -Recurse -File |
Where-Object { $_.Name -ieq "Divine.exe" } |
Select-Object -First 1
if (-not $divine) {
throw "Divine.exe was not found in the downloaded LSLib release."
}
& ".\\scripts\\build.ps1" -DivinePath $divine.FullName -Workspace "${{ github.workspace }}" -VersionTag "${{ steps.vars.outputs.version_tag }}"
- name: Show build result
run: |
$ErrorActionPreference = "Stop"
$archiveBaseName = "DnD 5.5e AIO Russian"
if ($env:GITHUB_REF -like "refs/tags/v*") {
$archiveBaseName = "DnD 5.5e AIO Russian $env:GITHUB_REF_NAME"
}
$zipPath = [System.IO.Path]::GetFullPath((Join-Path "${{ github.workspace }}" "build/$archiveBaseName.zip"))
Get-ChildItem "build/DnD 5.5e AIO Russian.pak", "build/info.json", $zipPath |
Select-Object FullName, Length, LastWriteTime
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: bg3-dnd55e-russian-localization-${{ github.run_number }}
path: build/*
if-no-files-found: error
- name: Create or update GitHub release
if: startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = "Stop"
$tagName = "${{ github.ref_name }}"
$zipPath = Join-Path $env:GITHUB_WORKSPACE "build\\DnD 5.5e AIO Russian $tagName.zip"
if (-not (Test-Path -LiteralPath $zipPath)) {
throw "Release archive was not found at '$zipPath'."
}
gh release view $tagName --repo "${{ github.repository }}" *> $null
if ($LASTEXITCODE -ne 0) {
gh release create $tagName $zipPath `
--repo "${{ github.repository }}" `
--title $tagName `
--notes ""
}
else {
gh release upload $tagName $zipPath `
--repo "${{ github.repository }}" `
--clobber
}

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
build/ build/
build-stage* build-stage*
.tools/ .tools/
.cache/
*.pak *.pak
*.tmp *.tmp
*.temp *.temp

137
ACTIONS.md Normal file
View File

@@ -0,0 +1,137 @@
# ACTIONS.md
VERSION: 3
MODE: machine-first
LANG: ru
ROUTING:
- match_user_request_to_action_id: true
- if_match: propose_action
- if_no_match: ignore_actions_md
- if_no_match_user_message: none
PROPOSE_RULE:
- prompt_template: "Приступить к выполнению '{action_id}'?"
- require_user_confirmation: true
- execute_without_confirmation: false
EXECUTION_BASELINE:
- enforce_agents_md: true
- minimal_non_breaking_changes: true
- steps_count_range: [3, 7]
- before_commit_push: request_user_approval
- prefer_existing_repo_scripts_over_manual_work: true
REPORT_FORMAT:
- done
- changed_files
- checks
- remaining
ACTIONS:
translation:diff:
intent: fetch_upstream_english_and_compare_with_ru
inputs:
- AGENTS.md::Canonical Paths::Upstream English reference
- Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
plan:
- run_scripts/get-upstream-english.ps1_and_wait_until_output_exists
- run_scripts/compare-translation.ps1_after_upstream_download_only
- classify_diff_into_missing_changed_stale
- write_machine_readable_and_markdown_reports_for_local_review
checks:
- xml_valid
- cache_path_gitignored
- local_only_no_ci_workflow_required
- translation_steps_not_parallelized_when_file_dependency_exists
outputs:
- .cache/upstream/english.xml
- build/translation-diff/summary.json
- build/translation-diff/summary.md
- build/translation-diff/candidates.json
translation:apply:
intent: apply_translation_edits_to_russian_xml
inputs:
- Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
- build/translation-diff/candidates.json
- prepared_update_texts_for_updates_and_optional_adds
plan:
- create_temporary_copy_of_russian_xml
- load_candidate_edit_file_and_temporary_xml
- fail_if_add_entry_has_empty_text
- apply_updates_and_optional_new_entries_by_contentuid
- write_utf8_bom_xml_to_temporary_copy
- validate_temporary_xml_via_separate_script
- replace_original_russian_xml_after_successful_validation
- report_changed_entries
checks:
- xml_valid
- contentuid_uniqueness_preserved
- only_requested_entries_changed
- no_partial_replace_on_validation_failure
outputs:
- Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
translation:update:
intent: sync_ru_translation_with_upstream
inputs:
- Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
- glossary/glossary.normalized.json
- AGENTS.md::Canonical Paths::Upstream English reference
plan:
- run_translation:diff_sequentially
- if_summary_has_no_missing_no_version_mismatch_no_stale_report_translation_up_to_date_and_stop
- if_diff_exists_stop_after_generating_build/translation-diff/candidates.json_until_prepared_edits_are_provided_explicitly
- review_build/translation-diff/candidates.json_before_apply
- reuse_glossary_for_term_consistency_when_preparing_texts
- run_translation:apply_only_after_candidate_texts_are_filled_and_explicit_edits_path_is_passed
checks:
- xml_valid
- glossary_consistency
- scope_limited_to_localization_and_allowed_metadata
- no_upstream_download_compare_race_condition
outputs:
- message: translation_up_to_date
- build/translation-diff/summary.json
- build/translation-diff/summary.md
- build/translation-diff/candidates.json
- Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml
- optional: Mods/DnD 5.5e AIO Russian/meta.lsx (release-only)
after_success:
- suggest_action: meta:sync-parent
reason: "Обновить версию зависимости из родительского мода (актуальный Version64 и связанные поля зависимости)."
action:report:
intent: unified_task_report
inputs:
- task_context
- modified_files
- verification_results
plan:
- summarize_done
- list_changed_files
- list_checks
- list_remaining
checks:
- concise
- factual
- no_unverified_claims
outputs:
- final_user_report
meta:sync-parent:
intent: sync_dependency_moduleshortdesc_from_parent_meta
inputs:
- parent_meta_git_url (optional; defaults to upstream)
- Mods/DnD 5.5e AIO Russian/meta.lsx
plan:
- read_parent_moduleinfo_fields
- validate_required_fields_folder_md5_name_publishhandle_uuid_version64
- update_target_dependencies_moduleshortdesc_fields
- validate_xml_structure
- report_changed_fields
checks:
- xml_valid
- required_parent_fields_present
- only_dependencies_moduleshortdesc_changed
outputs:
- Mods/DnD 5.5e AIO Russian/meta.lsx

278
AGENTS.md
View File

@@ -1,152 +1,228 @@
# AGENTS.md # AGENTS.md
## Project Overview ## Execution Model (MUST)
- Read this file first; treat as system-level constraints.
- Priority:
1. User instructions
2. AGENTS.md
3. Existing code/style
4. Best practices
- Prefer minimal, non-breaking changes.
- Do not introduce unnecessary abstractions.
This repository contains a standalone Russian localization mod for **Baldur's Gate 3**: ---
- Mod name: `DnD 5.5e All-in-One BEYOND Russian Localization` ## Communication (MUST)
- Mod folder: `Mods/DnD 5.5e AIO Russian` - Answer first, then request approval if needed.
- Base/original mod dependency: `DnD 5.5e All-in-One BEYOND` - Concise, meaningful, no filler.
- Original mod repository: `https://github.com/Yoonmoonsik/dnd55e` - Do not end response with only procedural choice.
- Original dependency UUID: `897914ef-5c96-053c-44af-0be823f895fe`
This repository is for the localization mod only. It must not gain gameplay logic, Script Extender files, or unrelated assets. Approval/clarification:
- ask once, no repetition
- binary → yes/no
- multiple → numbered options + brief context
## Repository Rules - File links in repo docs/checklists: relative paths, `/`, spaces as `%20`.
- In assistant UI responses, use the link format required by the execution environment; include relative path text when possible.
- Keep the repository source-only. ---
- Do not commit `.pak` artifacts.
- Do not commit temporary build outputs.
- Do not add gameplay or script content unrelated to localization/release packaging.
- Keep the localization folder and metadata consistent with the packaged mod.
## Current Important Paths ## Git Workflow (MUST)
- Never commit/push without explicit user approval.
- After approval → commit + push immediately.
- Commit messages: Russian, factual (what was done).
- Branch (`fix/*` or `feat/*`): ask once before the first file-changing task that may lead to commit; reuse decision for all subsequent tasks in same dialogue.
- Mod sources: `Mods/DnD 5.5e AIO Russian` After work in `fix/*` or `feat/*`:
- Localization XML: `Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml` 1. MR → main
- Mod metadata: `Mods/DnD 5.5e AIO Russian/meta.lsx` 2. merge → main + delete branch
- CI workflow: `.gitea/workflows/build.yml`
- Main build script: `scripts/build.ps1`
## Build And Release Model Push failure:
- retry ≤2 times, 3s delay
The authoritative build logic lives in: Release link:
- provide `[version](url)` immediately after tag push, without waiting for CI
- `scripts/build.ps1` ---
The Gitea workflow should stay thin and only: ## Scope (MUST)
- Repo = Russian localization mod only.
1. prepare the workspace Allowed:
2. download `Divine` - localization content
3. call `scripts/build.ps1` - packaging/release metadata
4. publish the release zip for tag builds
### Current Build Outputs Forbidden:
- gameplay logic
- Script Extender
- unrelated assets
The build script produces: - Repo must remain source-only.
- Never commit `.pak` or build artifacts.
- `build/DnD 5.5e AIO Russian.pak` ---
## Paths (MUST)
- Mod: `Mods/DnD 5.5e AIO Russian`
- Localization: `Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml`
- Metadata: `Mods/DnD 5.5e AIO Russian/meta.lsx`
- Build: `scripts/build.ps1` _(single source of build truth)_
- CI: `.gitea/workflows/build.yml`
- Glossary: `glossary/glossary.normalized.json` _(primary terminology reference)_
- Actions: `ACTIONS.md`
- Upstream EN reference: `https://github.com/Yoonmoonsik/dnd55e/blob/main/Mods/DnD2024_897914ef-5c96-053c-44af-0be823f895fe/Localization/English/english.xml`
---
## Packaging (MUST)
- `.pak` contains ONLY `Mods/...`
Required:
- `meta.lsx`
- `russian.xml`
Forbidden in `.pak`:
- `.git`, `.gitea`
- `scripts`, `tools`, `.tools`
- `build`, staging dirs
Staging:
- use `%TEMP%`
- not inside repo
---
## Build & CI (MUST)
Flow:
1. prepare
2. download Divine
3. run `scripts/build.ps1`
4. publish
Outputs:
- `build/*.pak`
- `build/info.json` - `build/info.json`
- `build/DnD 5.5e AIO Russian <tag>.zip` for tagged builds - `build/*.zip` (tag only)
The release zip is expected to contain: Release ZIP:
- only `.pak` + `info.json`
- the built `.pak` Triggers:
- `info.json` - automatic: push tag `v*`
- manual: workflow_dispatch
- branch pushes without tag MUST NOT publish release artifacts
## Packaging Notes ---
The package must contain only the BG3 mod structure under `Mods/...`. ## Versioning (CRITICAL)
Source of truth:
`ModuleInfo/Version64` — read via explicit XML parsing:
`save/region[@id="Config"]/node[@id="root"]/children/node[@id="ModuleInfo"]/attribute[@id="Version64"]`
Verified expected extracted `.pak` structure: Rules:
- do not change `PublishVersion`
- tag MUST match version
- `Mods/DnD 5.5e AIO Russian/meta.lsx` Before tag:
- `Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml` 1. if version already changed → use it
2. if same as last → bump:
`scripts/set-version.ps1 -VersionTag <tag>`
Do not allow `.git`, `.gitea`, `scripts`, `tools`, `.tools`, `build`, or staging directories into the `.pak`. `build.ps1`:
- derives version from tag
- writes to `info.json` + staged `meta.lsx`
## Important Packaging Behavior Conflict resolution (MUST):
- before release, `Version64` in `meta.lsx` MUST equal target tag version
- if mismatch, run `scripts/set-version.ps1 -VersionTag <tag>` and re-check
- if still mismatch, release is blocked
There is a runner-specific packaging quirk: ---
- `Divine` can produce a broken 48-byte `.pak` on the CI runner depending on the source path. ## info.json (MUST)
- Current mitigation is implemented in `scripts/build.ps1`. Root:
- The script uses staged sources and fallback packaging attempts. - `Mods`, `MD5`
- Staging is performed in `%TEMP%`, not in a dot-prefixed directory inside the repo.
If packaging breaks again, debug the source path and unpack the resulting `.pak` locally to verify actual contents. Per mod:
- Author, Name, Folder, Version
- Description, UUID, Created
- Dependencies (array), Group
## Versioning Dependency UUID:
`897914ef-5c96-053c-44af-0be823f895fe`
Version displayed by BG3ModManager should be derived from the release tag. ---
Current behavior: ## Guardrails (MUST)
- `scripts/build.ps1` derives `Version64` from tags like `v0.1.0` Before commit:
- the computed version is written into: - scope valid (localization/metadata only)
- generated `info.json` - no forbidden content
- staged `meta.lsx` before packaging - no build artifacts (`.pak`, `build/`, staging)
- no temp/debug artifacts; ignored patterns MUST be present in `.gitignore`: `build/`, `build-stage*`, `.tools/`, `*.pak`
- packaging invariants intact
- version consistent (if applicable)
Do not manually hardcode release versions in the committed `meta.lsx` for each release if CI can derive them from tags. Before push:
- explicit user approval
- commit message valid (RU, factual)
## info.json Expectations Before release:
- version == tag
- version bumped if needed
- CI/build contract valid
- outputs correct (no extra files)
`info.json` is generated during build and should remain aligned with BG3/BG3ModManager expectations. ---
Current expected shape: ## Release & Changelog (MUST)
- top-level `Mods` - Every release MUST include changelog.
- top-level `MD5`
- per-mod fields:
- `Author`
- `Name`
- `Folder`
- `Version`
- `Description`
- `UUID`
- `Created`
- `Dependencies`
- `Group`
Current dependency model: Changelog:
- language: Russian
- concise, user-facing
- describe WHAT changed
- group logically
- `Dependencies` is an array of dependency UUIDs Sources:
- current dependency UUID: - prefer diff over commits
- `897914ef-5c96-053c-44af-0be823f895fe`
## CI Trigger Policy Diff rules:
- inspect real file changes
Current workflow policy: Localization (`russian.xml`):
- added / changed / removed strings
- summarize user-visible impact (UI, spells, descriptions)
- run on tags `v*` Metadata / CI:
- run on manual dispatch - describe effect, not raw edits
- do not run on every push to `main`
## Git / Collaboration Preferences Large diff:
- group + summarize
User preference: If no visible changes:
- state "техническое обновление"
- after making changes, ask for permission before committing Before release:
- if the user approves, commit and push immediately - generate changelog draft
- for significant changes, propose moving work into a separate branch - ask for approval
- feature/fix branches must use the prefix `feat/` or `fix/`
- after finishing work in a `feat/` or `fix/` branch, propose merging it back into `main`
- comments and commit messages should be written in Russian
- commit messages should describe what was done, not what should be done
- if changes affect files that go into the final `.pak`, or change the build/release process, propose releasing the next version
- if push fails, retry up to two more times with a 3-second pause between attempts
Do not auto-commit or auto-push without explicit user approval. Approval gates:
- Gate A: explicit approval for commit/push (code/content changes)
- Gate B: explicit approval for release publish (after changelog draft)
## Cleanup Expectations Release message:
- version
- changelog
- `[version](url)` if derivable
Temporary directories and debug artifacts should not remain in the repository. Do not:
- invent changes
- include internal noise
Ignored paths currently include: ---
- `build/` ## Rules Maintenance (MUST)
- `build-stage*` - Changes to `AGENTS.md` / `ACTIONS.md`: prefer compressed, machine-readable edits.
- `.tools/` - Keep updates minimal and non-duplicative: merge overlapping points, remove redundancy, preserve intent.
- `*.pak`
If local debugging creates additional temporary folders, remove them when done unless the user explicitly wants to keep them.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<save> <save>
<version major="4" minor="8" revision="0" build="500"/> <version major="4" minor="8" revision="0" build="500"/>
<region id="Config"> <region id="Config">
<node id="root"> <node id="root">
<children> <children>
<node id="Dependencies"> <node id="Conflicts"/>
<children> <node id="Dependencies">
<node id="ModuleShortDesc"> <children>
<attribute id="Folder" type="LSString" value=""/> <node id="ModuleShortDesc">
<attribute id="MD5" type="LSString" value=""/> <attribute id="Folder" type="LSString" value="DnD2024_897914ef-5c96-053c-44af-0be823f895fe"/>
<attribute id="Name" type="LSString" value="DnD 5.5e All-in-One BEYOND"/> <attribute id="MD5" type="LSString" value="4bd42ca93f895d1ec521a286bea09ef2"/>
<attribute id="PublishHandle" type="uint64" value="0"/> <attribute id="Name" type="LSString" value="DnD 5.5e All-in-One BEYOND"/>
<attribute id="UUID" type="guid" value="897914ef-5c96-053c-44af-0be823f895fe"/> <attribute id="PublishHandle" type="uint64" value="4419649"/>
<attribute id="Version64" type="int64" value="36028797018963968"/> <attribute id="UUID" type="guid" value="897914ef-5c96-053c-44af-0be823f895fe"/>
</node> <attribute id="Version64" type="int64" value="144396675937468416"/>
</children> </node>
</children>
</node>
<node id="ModuleInfo">
<attribute id="Author" type="LSString" value="Underslumber Team"/>
<attribute id="CharacterCreationLevelName" type="FixedString" value=""/>
<attribute id="Description" type="LSString" value="Русский перевод мода DnD 5.5e All-in-One BEYOND. Перевод ещё в разработке: AI помогает быстро обновлять тексты, а финальные правки и качество мы проверяем вручную."/>
<attribute id="FileSize" type="uint64" value="2488095"/>
<attribute id="Folder" type="LSString" value="DnD 5.5e AIO Russian"/>
<attribute id="LobbyLevelName" type="FixedString" value=""/>
<attribute id="MD5" type="LSString" value="c0a8f3412870277331306e0719fc6f77"/>
<attribute id="MenuLevelName" type="FixedString" value=""/>
<attribute id="Name" type="LSString" value="DnD 5.5e All-in-One BEYOND Russian Localization"/>
<attribute id="NumPlayers" type="uint8" value="4"/>
<attribute id="PhotoBooth" type="FixedString" value=""/>
<attribute id="PublishHandle" type="uint64" value="5965149"/>
<attribute id="StartupLevelName" type="FixedString" value=""/>
<attribute id="UUID" type="FixedString" value="6401e84d-daf2-416d-adeb-99c03a2487a6"/>
<attribute id="Version64" type="int64" value="281487861612544"/>
<children>
<node id="PublishVersion">
<attribute id="Version64" type="int64" value="281477124194304"/>
</node>
<node id="Scripts">
<children>
<node id="Script">
<attribute id="UUID" type="FixedString" value="1953f77d-a201-45d7-a194-9b84c34b8461"/>
<children>
<node id="Parameters">
<children>
<node id="Parameter">
<attribute id="MapKey" type="FixedString" value="HardcoreOnly"/>
<attribute id="Type" type="int32" value="1"/>
<attribute id="Value" type="LSString" value="0"/>
</node>
</children>
</node>
</children>
</node>
<node id="Script">
<attribute id="UUID" type="FixedString" value="0d6510f5-50a3-4ecd-83d8-134c9a640324"/>
<children>
<node id="Parameters">
<children>
<node id="Parameter">
<attribute id="MapKey" type="FixedString" value="HardcoreOnly"/>
<attribute id="Type" type="int32" value="1"/>
<attribute id="Value" type="LSString" value="0"/>
</node>
</children>
</node>
</children>
</node>
</children>
</node>
</children>
</node>
</children>
</node> </node>
<node id="ModuleInfo"> </region>
<attribute id="Author" type="LSString" value="MikhailRaw"/>
<attribute id="CharacterCreationLevelName" type="FixedString" value=""/>
<attribute id="Description" type="LSString" value="Русская локализация мода, который добавляет и обновляет контент в соответствии с правилами DnD 5.5e и другими источниками, включая предыстории, классы, таланты, расы, заклинания и многое другое. Это отдельный мод локализации и он требует установленный оригинальный мод."/>
<attribute id="FileSize" type="uint64" value="0"/>
<attribute id="Folder" type="LSString" value="DnD 5.5e AIO Russian"/>
<attribute id="LobbyLevelName" type="FixedString" value=""/>
<attribute id="MD5" type="LSString" value=""/>
<attribute id="MenuLevelName" type="FixedString" value=""/>
<attribute id="Name" type="LSString" value="DnD 5.5e All-in-One BEYOND Russian Localization"/>
<attribute id="NumPlayers" type="uint8" value="4"/>
<attribute id="PhotoBooth" type="FixedString" value=""/>
<attribute id="PublishHandle" type="uint64" value="0"/>
<attribute id="StartupLevelName" type="FixedString" value=""/>
<attribute id="UUID" type="FixedString" value="6401e84d-daf2-416d-adeb-99c03a2487a6"/>
<attribute id="Version64" type="int64" value="36028797018963968"/>
<children>
<node id="PublishVersion">
<attribute id="Version64" type="int64" value="36028797018963968"/>
</node>
</children>
</node>
</children>
</node>
</region>
</save> </save>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -1,12 +1,11 @@
# DnD 5.5e All-in-One BEYOND Russian Localization # DnD 5.5e All-in-One BEYOND Russian Localization
Русский перевод мода **DnD 5.5e All-in-One BEYOND** для **Baldur's Gate 3**. Русская локализация [**DnD 5.5e All-in-One BEYOND**](https://github.com/Yoonmoonsik/dnd55e) для **Baldur's Gate 3**.
Оригинальный репозиторий: Публикация на [mod.io](https://mod.io/g/baldursgate3/m/dnd-55e-all-in-one-beyond-russian-localization).
[Yoonmoonsik/dnd55e](https://github.com/Yoonmoonsik/dnd55e)
## О моде ## О моде
**DnD 5.5e All-in-One BEYOND** переносит в Baldur's Gate 3 контент и механики, вдохновлённые Dungeons & Dragons 5.5e. Мод расширяет игру новыми и переработанными элементами, включая классы, предыстории, таланты, расы, заклинания и другие связанные системы. Русский перевод мода **DnD 5.5e All-in-One BEYOND**, который переносит в **Baldur's Gate 3** широкий пласт контента и правил **D&D 5.5e / PHB 2024**.
Этот проект предназначен для русской локализации оригинального мода и его текстового контента. Локализация поддерживается в темпе с апстримом, который сейчас развивается в сторону более полного охвата классов, рас, предысторий, фитов и заклинаний.

View File

@@ -459,7 +459,7 @@
"Locate animals or plants": "Поиск животных или растений", "Locate animals or plants": "Поиск животных или растений",
"Locate creature": "Поиск существа", "Locate creature": "Поиск существа",
"Locate object": "Поиск предмета", "Locate object": "Поиск предмета",
"Long Rest": "долгий отдых", "Long Rest": "Долгий отдых",
"Longbow": "Длинный лук", "Longbow": "Длинный лук",
"Longstrider": "Скороход", "Longstrider": "Скороход",
"Longsword": "Длинный меч", "Longsword": "Длинный меч",
@@ -556,6 +556,10 @@
"Origin Feat: Alert": "Черта происхождения: Бдительный", "Origin Feat: Alert": "Черта происхождения: Бдительный",
"Origin Feat: Magic Initiate (Cleric)": "Черта происхождения: Посвященный в магию (Жрец)", "Origin Feat: Magic Initiate (Cleric)": "Черта происхождения: Посвященный в магию (Жрец)",
"Origin Feat: Magic Initiate (Wizard)": "Черта происхождения: Посвященный в магию (Волшебник)", "Origin Feat: Magic Initiate (Wizard)": "Черта происхождения: Посвященный в магию (Волшебник)",
"Origin Feat: Magic Initiate (Druid)": "Черта происхождения: Посвященный в магию (Друид)",
"Origin Feat: Healer": "Черта происхождения: Лекарь",
"Origin Feat: Lucky": "Черта происхождения: Везунчик",
"Origin Feat: Tavern Brawler": "Черта происхождения: Драчун",
"Origin Feat: Musician": "Черта происхождения: Музыкант", "Origin Feat: Musician": "Черта происхождения: Музыкант",
"Origin Feat: Savage Attacker": "Черта происхождения: Дикий атакующий", "Origin Feat: Savage Attacker": "Черта происхождения: Дикий атакующий",
"Origin Feat: Skilled": "Черта происхождения: Одаренный", "Origin Feat: Skilled": "Черта происхождения: Одаренный",
@@ -691,7 +695,7 @@
"Shillelagh": "Шиллейла", "Shillelagh": "Шиллейла",
"Shocking Grasp": "Шоковое прикосновение", "Shocking Grasp": "Шоковое прикосновение",
"Shocking grasp": "Электрошок", "Shocking grasp": "Электрошок",
"Short Rest": "короткий отдых", "Short Rest": "Короткий отдых",
"Shortbow": "Короткий лук", "Shortbow": "Короткий лук",
"Shortsword": "Короткий меч", "Shortsword": "Короткий меч",
"Sickle": "Серп", "Sickle": "Серп",

View File

@@ -0,0 +1,210 @@
param(
[string]$RussianPath = "Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml",
[Parameter(Mandatory = $true)]
[string]$EditsPath
)
$ErrorActionPreference = "Stop"
function Read-XmlDocument {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$resolvedPath = [System.IO.Path]::GetFullPath($Path)
if (-not (Test-Path -LiteralPath $resolvedPath)) {
throw "XML file was not found: '$resolvedPath'."
}
[xml]$xml = Get-Content -LiteralPath $resolvedPath -Raw
if ($null -eq $xml.SelectSingleNode('/contentList')) {
throw "XML file does not contain '/contentList': '$resolvedPath'."
}
return @{
Path = $resolvedPath
Xml = $xml
}
}
function Get-ContentNodeMap {
param(
[Parameter(Mandatory = $true)]
[xml]$Xml
)
$map = @{}
foreach ($node in $Xml.SelectNodes('/contentList/content')) {
$contentUid = [string]$node.GetAttribute("contentuid")
if ([string]::IsNullOrWhiteSpace($contentUid)) {
continue
}
if ($map.ContainsKey($contentUid)) {
throw "Duplicate contentuid found in target XML: '$contentUid'."
}
$map[$contentUid] = $node
}
return $map
}
function Assert-UniqueEditContentUid {
param(
[AllowEmptyCollection()]
[Parameter(Mandatory = $true)]
[System.Collections.Generic.HashSet[string]]$Seen,
[Parameter(Mandatory = $true)]
[string]$ContentUid,
[Parameter(Mandatory = $true)]
[string]$Section
)
if (-not $Seen.Add($ContentUid)) {
throw "Edits file contains duplicate contentuid '$ContentUid' in '$Section'."
}
}
$russianDocument = Read-XmlDocument -Path $RussianPath
$temporaryRussianPath = "$($russianDocument.Path).tmp"
$validateScriptPath = Join-Path $PSScriptRoot "validate-translation-xml.ps1"
if (-not (Test-Path -LiteralPath $validateScriptPath)) {
throw "Validation script was not found: '$validateScriptPath'."
}
if (Test-Path -LiteralPath $temporaryRussianPath) {
Remove-Item -LiteralPath $temporaryRussianPath -Force
}
Copy-Item -LiteralPath $russianDocument.Path -Destination $temporaryRussianPath -Force
$russianDocument = Read-XmlDocument -Path $temporaryRussianPath
$resolvedEditsPath = [System.IO.Path]::GetFullPath($EditsPath)
if (-not (Test-Path -LiteralPath $resolvedEditsPath)) {
throw "Edits file was not found: '$resolvedEditsPath'."
}
$edits = Get-Content -LiteralPath $resolvedEditsPath -Raw | ConvertFrom-Json -Depth 10
if ($null -eq $edits) {
throw "Edits file is empty or invalid JSON: '$resolvedEditsPath'."
}
$contentListNode = $russianDocument.Xml.SelectSingleNode('/contentList')
if ($null -eq $contentListNode) {
throw "Target russian.xml does not contain '/contentList': '$($russianDocument.Path)'."
}
$nodeMap = Get-ContentNodeMap -Xml $russianDocument.Xml
$updatedEntries = New-Object System.Collections.Generic.List[string]
$addedEntries = New-Object System.Collections.Generic.List[string]
$seenEditContentUids = [System.Collections.Generic.HashSet[string]]::new()
$updates = @()
if ($edits.PSObject.Properties.Name -contains "updates" -and $null -ne $edits.updates) {
$updates = @($edits.updates)
}
foreach ($edit in $updates) {
$contentUid = [string]$edit.contentuid
if ([string]::IsNullOrWhiteSpace($contentUid)) {
throw "Each update entry must contain non-empty 'contentuid'."
}
Assert-UniqueEditContentUid -Seen $seenEditContentUids -ContentUid $contentUid -Section "updates"
if (-not $nodeMap.ContainsKey($contentUid)) {
throw "Target russian.xml does not contain contentuid '$contentUid' for update."
}
$node = $nodeMap[$contentUid]
if ($edit.PSObject.Properties.Name -contains "version" -and -not [string]::IsNullOrWhiteSpace([string]$edit.version)) {
$node.SetAttribute("version", [string]$edit.version)
}
if ($edit.PSObject.Properties.Name -contains "text") {
if ([string]::IsNullOrWhiteSpace([string]$edit.text)) {
throw "Update entry '$contentUid' must contain non-empty 'text' when provided."
}
$node.InnerText = [string]$edit.text
}
$updatedEntries.Add($contentUid) | Out-Null
}
$adds = @()
if ($edits.PSObject.Properties.Name -contains "adds" -and $null -ne $edits.adds) {
$adds = @($edits.adds)
}
if (($updates.Count -eq 0) -and ($adds.Count -eq 0)) {
if (Test-Path -LiteralPath $temporaryRussianPath) {
Remove-Item -LiteralPath $temporaryRussianPath -Force
}
Write-Host "[apply-translation-edits.ps1] No edits requested. Original russian.xml left unchanged."
return
}
foreach ($edit in $adds) {
$contentUid = [string]$edit.contentuid
$version = [string]$edit.version
$text = [string]$edit.text
if ([string]::IsNullOrWhiteSpace($contentUid)) {
throw "Each add entry must contain non-empty 'contentuid'."
}
Assert-UniqueEditContentUid -Seen $seenEditContentUids -ContentUid $contentUid -Section "adds"
if ($nodeMap.ContainsKey($contentUid)) {
throw "Target russian.xml already contains contentuid '$contentUid'; use 'updates' instead of 'adds'."
}
if ([string]::IsNullOrWhiteSpace($version)) {
throw "Add entry '$contentUid' must contain non-empty 'version'."
}
if ([string]::IsNullOrWhiteSpace($text)) {
throw "Add entry '$contentUid' must contain non-empty 'text'."
}
$newNode = $russianDocument.Xml.CreateElement("content")
$newNode.SetAttribute("contentuid", $contentUid)
$newNode.SetAttribute("version", $version)
$newNode.InnerText = $text
[void]$contentListNode.AppendChild($newNode)
$nodeMap[$contentUid] = $newNode
$addedEntries.Add($contentUid) | Out-Null
}
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $true
$settings.IndentChars = " "
$settings.NewLineChars = "`n"
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$writer = [System.Xml.XmlWriter]::Create($russianDocument.Path, $settings)
try {
$russianDocument.Xml.WriteTo($writer)
} finally {
$writer.Dispose()
}
try {
& $validateScriptPath -XmlPath $temporaryRussianPath
Move-Item -LiteralPath $temporaryRussianPath -Destination $RussianPath -Force
} finally {
if (Test-Path -LiteralPath $temporaryRussianPath) {
Remove-Item -LiteralPath $temporaryRussianPath -Force
}
}
Write-Host "[apply-translation-edits.ps1] Updated entries: $($updatedEntries.Count); Added entries: $($addedEntries.Count)."
if ($updatedEntries.Count -gt 0) {
Write-Host "[apply-translation-edits.ps1] Updated contentuid: $($updatedEntries -join ', ')"
}
if ($addedEntries.Count -gt 0) {
Write-Host "[apply-translation-edits.ps1] Added contentuid: $($addedEntries -join ', ')"
}

View File

@@ -7,8 +7,8 @@ param(
[string]$ArchiveBaseName = "DnD 5.5e AIO Russian", [string]$ArchiveBaseName = "DnD 5.5e AIO Russian",
[string]$ModName = "DnD 5.5e All-in-One BEYOND Russian Localization", [string]$ModName = "DnD 5.5e All-in-One BEYOND Russian Localization",
[string]$ModUuid = "6401e84d-daf2-416d-adeb-99c03a2487a6", [string]$ModUuid = "6401e84d-daf2-416d-adeb-99c03a2487a6",
[string]$ModAuthor = "MikhailRaw", [string]$ModAuthor = "Underslumber Team",
[string]$ModDescription = "Russian Localization", [string]$ModDescription = "Русская локализация мода, который добавляет и обновляет контент в соответствии с правилами DnD 5.5e и другими источниками, включая предыстории, классы, таланты, расы, заклинания и многое другое. Это отдельный мод локализации и он требует установленный оригинальный мод.",
[string]$ModVersion64 = "36028797018963968", [string]$ModVersion64 = "36028797018963968",
[string]$ModGroup = "6401e84d-daf2-416d-adeb-99c03a2487a6", [string]$ModGroup = "6401e84d-daf2-416d-adeb-99c03a2487a6",
[string]$DependencyUuid = "897914ef-5c96-053c-44af-0be823f895fe", [string]$DependencyUuid = "897914ef-5c96-053c-44af-0be823f895fe",
@@ -101,9 +101,10 @@ if (-not (Test-Path -LiteralPath $stagedMetaPath)) {
throw "Staged meta.lsx was not found: '$stagedMetaPath'." throw "Staged meta.lsx was not found: '$stagedMetaPath'."
} }
$stagedMetaContent = Get-Content -LiteralPath $stagedMetaPath -Raw $utf8Encoding = [System.Text.UTF8Encoding]::new($false)
$stagedMetaContent = [System.IO.File]::ReadAllText($stagedMetaPath, $utf8Encoding)
$stagedMetaContent = $stagedMetaContent -replace '(<attribute id="Version64" type="int64" value=")\d+("/>)', "`${1}$resolvedVersion64`${2}" $stagedMetaContent = $stagedMetaContent -replace '(<attribute id="Version64" type="int64" value=")\d+("/>)', "`${1}$resolvedVersion64`${2}"
Set-Content -LiteralPath $stagedMetaPath -Value $stagedMetaContent -Encoding utf8 [System.IO.File]::WriteAllText($stagedMetaPath, $stagedMetaContent, $utf8Encoding)
Write-Host "[build.ps1] Staged source tree:" Write-Host "[build.ps1] Staged source tree:"
Get-ChildItem -Recurse $stagingPath | Select-Object FullName, Length | Format-Table -AutoSize Get-ChildItem -Recurse $stagingPath | Select-Object FullName, Length | Format-Table -AutoSize
@@ -112,6 +113,8 @@ if (Test-Path -LiteralPath $tempPackagePath) {
Remove-Item -LiteralPath $tempPackagePath -Force Remove-Item -LiteralPath $tempPackagePath -Force
} }
# CI quirk: Divine can occasionally emit a broken ~48-byte package for some source roots.
# Mitigation: try staged/mods/workspace sources and accept only outputs that look valid by size.
$packageAttempts = @( $packageAttempts = @(
[ordered]@{ Name = "staging-root"; Source = $stagingPath }, [ordered]@{ Name = "staging-root"; Source = $stagingPath },
[ordered]@{ Name = "mods-root"; Source = $modsPath }, [ordered]@{ Name = "mods-root"; Source = $modsPath },

View File

@@ -0,0 +1,209 @@
param(
[string]$EnglishPath = ".cache/upstream/english.xml",
[string]$RussianPath = "Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml",
[string]$OutputDir = "build/translation-diff"
)
$ErrorActionPreference = "Stop"
function Get-LocalizationEntries {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$resolvedPath = [System.IO.Path]::GetFullPath($Path)
if (-not (Test-Path -LiteralPath $resolvedPath)) {
throw "Localization XML was not found: '$resolvedPath'."
}
[xml]$xml = Get-Content -LiteralPath $resolvedPath -Raw
$nodes = $xml.SelectNodes('/contentList/content')
if ($null -eq $nodes) {
throw "Localization XML does not contain '/contentList/content' nodes: '$resolvedPath'."
}
$entries = @{}
foreach ($node in $nodes) {
$contentUid = [string]$node.GetAttribute("contentuid")
if ([string]::IsNullOrWhiteSpace($contentUid)) {
throw "Localization XML contains a content node without 'contentuid': '$resolvedPath'."
}
if ($entries.ContainsKey($contentUid)) {
throw "Localization XML contains duplicate contentuid '$contentUid': '$resolvedPath'."
}
$version = [string]$node.GetAttribute("version")
if ([string]::IsNullOrWhiteSpace($version)) {
throw "Localization XML contains contentuid '$contentUid' with empty 'version': '$resolvedPath'."
}
$entries[$contentUid] = [ordered]@{
contentuid = $contentUid
version = $version
text = [string]$node.InnerText
}
}
return $entries
}
$resolvedOutputDir = [System.IO.Path]::GetFullPath($OutputDir)
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
$englishEntries = Get-LocalizationEntries -Path $EnglishPath
$russianEntries = Get-LocalizationEntries -Path $RussianPath
$missingInRussian = New-Object System.Collections.Generic.List[object]
$versionMismatch = New-Object System.Collections.Generic.List[object]
$staleOnlyInRussian = New-Object System.Collections.Generic.List[object]
foreach ($contentUid in ($englishEntries.Keys | Sort-Object)) {
$englishEntry = $englishEntries[$contentUid]
if (-not $russianEntries.ContainsKey($contentUid)) {
$missingInRussian.Add([pscustomobject]@{
contentuid = $contentUid
englishVersion = $englishEntry.version
englishText = $englishEntry.text
}) | Out-Null
continue
}
$russianEntry = $russianEntries[$contentUid]
if ($englishEntry.version -ne $russianEntry.version) {
$versionMismatch.Add([pscustomobject]@{
contentuid = $contentUid
englishVersion = $englishEntry.version
russianVersion = $russianEntry.version
englishText = $englishEntry.text
russianText = $russianEntry.text
}) | Out-Null
}
}
foreach ($contentUid in ($russianEntries.Keys | Sort-Object)) {
if (-not $englishEntries.ContainsKey($contentUid)) {
$russianEntry = $russianEntries[$contentUid]
$staleOnlyInRussian.Add([pscustomobject]@{
contentuid = $contentUid
russianVersion = $russianEntry.version
russianText = $russianEntry.text
}) | Out-Null
}
}
$summary = [ordered]@{
generatedAt = (Get-Date).ToString("o")
englishPath = [System.IO.Path]::GetFullPath($EnglishPath)
russianPath = [System.IO.Path]::GetFullPath($RussianPath)
englishCount = $englishEntries.Count
russianCount = $russianEntries.Count
missingInRussianCount = $missingInRussian.Count
versionMismatchCount = $versionMismatch.Count
staleOnlyInRussianCount = $staleOnlyInRussian.Count
missingInRussian = $missingInRussian
versionMismatch = $versionMismatch
staleOnlyInRussian = $staleOnlyInRussian
}
$summaryJsonPath = Join-Path $resolvedOutputDir "summary.json"
$summaryMdPath = Join-Path $resolvedOutputDir "summary.md"
$candidatesJsonPath = Join-Path $resolvedOutputDir "candidates.json"
$summary | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $summaryJsonPath -Encoding utf8
$candidates = [ordered]@{
generatedAt = (Get-Date).ToString("o")
source = [ordered]@{
englishPath = [System.IO.Path]::GetFullPath($EnglishPath)
russianPath = [System.IO.Path]::GetFullPath($RussianPath)
}
updates = @(
$versionMismatch | ForEach-Object {
[ordered]@{
contentuid = $_.contentuid
version = $_.englishVersion
text = $_.russianText
englishText = $_.englishText
russianVersion = $_.russianVersion
}
}
)
adds = @(
$missingInRussian | ForEach-Object {
[ordered]@{
contentuid = $_.contentuid
version = $_.englishVersion
text = ""
englishText = $_.englishText
}
}
)
}
$candidates | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $candidatesJsonPath -Encoding utf8
$isUpToDate = ($missingInRussian.Count -eq 0) -and ($versionMismatch.Count -eq 0) -and ($staleOnlyInRussian.Count -eq 0)
$mdLines = @(
"# Translation diff summary",
"",
"- Generated: $($summary.generatedAt)",
"- English entries: $($summary.englishCount)",
"- Russian entries: $($summary.russianCount)",
"- Missing in Russian: $($summary.missingInRussianCount)",
"- Version mismatches: $($summary.versionMismatchCount)",
"- Stale only in Russian: $($summary.staleOnlyInRussianCount)",
""
)
if ($isUpToDate) {
$mdLines += "Перевод уже актуален, дополнительные действия не требуются."
} else {
$mdLines += ""
$mdLines += "## Agent workflow"
$mdLines += "1. Refresh upstream cache: ``scripts/get-upstream-english.ps1``"
$mdLines += "2. Refresh diff reports: ``scripts/compare-translation.ps1``"
$mdLines += "3. Fill translated texts in ``build/translation-diff/candidates.json``"
$mdLines += "4. Apply only prepared edits: ``scripts/apply-translation-edits.ps1 -EditsPath build/translation-diff/candidates.json``"
}
$mdLines += ""
$mdLines += "## Missing in Russian"
if ($missingInRussian.Count -eq 0) {
$mdLines += "- none"
} else {
$mdLines += ($missingInRussian | Select-Object -First 50 | ForEach-Object {
"- ``$($_.contentuid)`` v$($_.englishVersion): $($_.englishText)"
})
}
$mdLines += ""
$mdLines += "## Version mismatches"
if ($versionMismatch.Count -eq 0) {
$mdLines += "- none"
} else {
$mdLines += ($versionMismatch | Select-Object -First 50 | ForEach-Object {
"- ``$($_.contentuid)`` en=v$($_.englishVersion), ru=v$($_.russianVersion)"
})
}
$mdLines += ""
$mdLines += "## Stale only in Russian"
if ($staleOnlyInRussian.Count -eq 0) {
$mdLines += "- none"
} else {
$mdLines += ($staleOnlyInRussian | Select-Object -First 50 | ForEach-Object {
"- ``$($_.contentuid)`` v$($_.russianVersion): $($_.russianText)"
})
}
Set-Content -LiteralPath $summaryMdPath -Value $mdLines -Encoding utf8
Write-Host "[compare-translation.ps1] Summary written to '$summaryJsonPath' and '$summaryMdPath'."
Write-Host "[compare-translation.ps1] Editable candidate file written to '$candidatesJsonPath'."
Write-Host "[compare-translation.ps1] Missing=$($missingInRussian.Count); VersionMismatch=$($versionMismatch.Count); StaleOnlyInRussian=$($staleOnlyInRussian.Count)."
if ($isUpToDate) {
Write-Host "[compare-translation.ps1] Перевод уже актуален, дополнительные действия не требуются."
}

View File

@@ -0,0 +1,44 @@
param(
[string]$UpstreamEnglishUrl = "https://raw.githubusercontent.com/Yoonmoonsik/dnd55e/main/Mods/DnD2024_897914ef-5c96-053c-44af-0be823f895fe/Localization/English/english.xml",
[string]$OutputPath = ".cache/upstream/english.xml",
[switch]$Force
)
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($UpstreamEnglishUrl)) {
throw "UpstreamEnglishUrl must not be empty."
}
$resolvedOutputPath = [System.IO.Path]::GetFullPath($OutputPath)
$outputDirectory = Split-Path -Parent $resolvedOutputPath
if (-not (Test-Path -LiteralPath $outputDirectory)) {
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
}
$requestParameters = @{
Uri = $UpstreamEnglishUrl
OutFile = $resolvedOutputPath
UseBasicParsing = $true
TimeoutSec = 120
}
if ($Force) {
$requestParameters["Headers"] = @{
"Cache-Control" = "no-cache"
}
}
Invoke-WebRequest @requestParameters
[xml]$englishXml = Get-Content -LiteralPath $resolvedOutputPath -Raw
$rootNode = $englishXml.SelectSingleNode('/contentList')
if ($null -eq $rootNode) {
throw "Downloaded file is not a valid BG3 localization XML: '$resolvedOutputPath'."
}
$contentCount = $englishXml.SelectNodes('/contentList/content').Count
$fileInfo = Get-Item -LiteralPath $resolvedOutputPath
Write-Host "[get-upstream-english.ps1] Saved upstream english.xml to '$resolvedOutputPath'."
Write-Host "[get-upstream-english.ps1] Entries: $contentCount; Size: $($fileInfo.Length) bytes; Updated: $($fileInfo.LastWriteTime.ToString("o"))."

63
scripts/set-version.ps1 Normal file
View File

@@ -0,0 +1,63 @@
param(
[Parameter(Mandatory = $true)]
[string]$VersionTag,
[string]$MetaPath = "Mods/DnD 5.5e AIO Russian/meta.lsx"
)
$ErrorActionPreference = "Stop"
function Convert-VersionTagToVersion64 {
param(
[string]$Tag
)
$normalized = $Tag
if ($normalized.StartsWith("v")) {
$normalized = $normalized.Substring(1)
}
if ($normalized -notmatch '^\d+(\.\d+){0,3}$') {
throw "Version tag '$Tag' is invalid. Expected format: vX.Y.Z or X.Y.Z"
}
$parts = $normalized.Split(".")
$numbers = @(0, 0, 0, 0)
for ($i = 0; $i -lt $parts.Length; $i++) {
$numbers[$i] = [int]$parts[$i]
}
return ([int64]$numbers[0] -shl 55) -bor ([int64]$numbers[1] -shl 47) -bor ([int64]$numbers[2] -shl 31) -bor [int64]$numbers[3]
}
$resolvedMetaPath = [System.IO.Path]::GetFullPath($MetaPath)
if (-not (Test-Path -LiteralPath $resolvedMetaPath)) {
throw "meta.lsx was not found: '$resolvedMetaPath'."
}
$resolvedVersion64 = Convert-VersionTagToVersion64 -Tag $VersionTag
$utf8Encoding = [System.Text.UTF8Encoding]::new($false)
$metaContent = [System.IO.File]::ReadAllText($resolvedMetaPath, $utf8Encoding)
[xml]$metaXml = $metaContent
# Explicitly target ModuleInfo/Version64 via XML path to avoid touching Dependencies/PublishVersion.
$moduleInfoVersionNode = $metaXml.SelectSingleNode('/save/region/node/children/node[@id="ModuleInfo"]/attribute[@id="Version64" and @type="int64"]')
if ($null -eq $moduleInfoVersionNode) {
throw "ModuleInfo/Version64 attribute was not found in '$resolvedMetaPath'."
}
# Replace only the Version64 attribute that appears inside ModuleInfo before its <children> block.
$moduleInfoVersionPattern = '(?s)(<node id="ModuleInfo">\s*(?:(?!<children>).)*?<attribute id="Version64" type="int64" value=")\d+("/>)'
if ($metaContent -notmatch $moduleInfoVersionPattern) {
throw "ModuleInfo/Version64 attribute was not found in '$resolvedMetaPath'."
}
$updatedMeta = [regex]::Replace(
$metaContent,
$moduleInfoVersionPattern,
"`${1}$resolvedVersion64`${2}",
1
)
[System.IO.File]::WriteAllText($resolvedMetaPath, $updatedMeta, $utf8Encoding)
Write-Host "[set-version.ps1] Updated '$resolvedMetaPath' to Version64=$resolvedVersion64 (from tag '$VersionTag')."

View File

@@ -0,0 +1,91 @@
param(
[string]$ParentMetaUrl = "https://raw.githubusercontent.com/Yoonmoonsik/dnd55e/main/Mods/DnD2024_897914ef-5c96-053c-44af-0be823f895fe/meta.lsx",
[string]$TargetMetaPath = "Mods/DnD 5.5e AIO Russian/meta.lsx"
)
$ErrorActionPreference = "Stop"
$resolvedTargetMetaPath = [System.IO.Path]::GetFullPath($TargetMetaPath)
if (-not (Test-Path -LiteralPath $resolvedTargetMetaPath)) {
throw "Target meta.lsx was not found: '$resolvedTargetMetaPath'."
}
if ([string]::IsNullOrWhiteSpace($ParentMetaUrl)) {
throw "ParentMetaUrl must not be empty."
}
try {
$parentResponse = Invoke-WebRequest -Uri $ParentMetaUrl -UseBasicParsing -TimeoutSec 60
} catch {
throw "Failed to download parent meta.lsx from '$ParentMetaUrl': $($_.Exception.Message)"
}
$parentRaw = $parentResponse.Content
$targetRaw = Get-Content -LiteralPath $resolvedTargetMetaPath -Raw
[xml]$parentXml = $parentRaw
[xml]$targetXml = $targetRaw
$parentModuleInfo = $parentXml.SelectSingleNode('/save/region/node/children/node[@id="ModuleInfo"]')
if ($null -eq $parentModuleInfo) {
throw "ModuleInfo node was not found in parent meta downloaded from '$ParentMetaUrl'."
}
$requiredFields = @("Folder", "MD5", "Name", "PublishHandle", "UUID", "Version64")
$sourceValues = @{}
foreach ($field in $requiredFields) {
$node = $parentModuleInfo.SelectSingleNode("attribute[@id='$field']")
if ($null -eq $node) {
throw "Required parent ModuleInfo attribute '$field' is missing in meta downloaded from '$ParentMetaUrl'."
}
$value = $node.GetAttribute("value")
if ([string]::IsNullOrWhiteSpace($value)) {
throw "Required parent ModuleInfo attribute '$field' has empty value in meta downloaded from '$ParentMetaUrl'."
}
$sourceValues[$field] = $value
}
$targetDependencyNode = $targetXml.SelectSingleNode('/save/region/node/children/node[@id="Dependencies"]/children/node[@id="ModuleShortDesc"]')
if ($null -eq $targetDependencyNode) {
throw "Dependencies/ModuleShortDesc node was not found in target meta: '$resolvedTargetMetaPath'."
}
$changedFields = @()
foreach ($field in $requiredFields) {
$targetAttr = $targetDependencyNode.SelectSingleNode("attribute[@id='$field']")
if ($null -eq $targetAttr) {
throw "Target Dependencies/ModuleShortDesc attribute '$field' is missing in '$resolvedTargetMetaPath'."
}
$currentValue = $targetAttr.GetAttribute("value")
$newValue = [string]$sourceValues[$field]
if ($currentValue -ne $newValue) {
$targetAttr.SetAttribute("value", $newValue)
$changedFields += $field
}
}
if ($changedFields.Count -eq 0) {
Write-Host "[sync-parent-meta.ps1] No changes needed. Target dependency data is already up to date."
} else {
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $utf8Bom
$settings.Indent = $true
$settings.IndentChars = " "
$settings.NewLineChars = "`n"
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$writer = [System.Xml.XmlWriter]::Create($resolvedTargetMetaPath, $settings)
try {
$targetXml.WriteTo($writer)
} finally {
$writer.Dispose()
}
Write-Host ("[sync-parent-meta.ps1] Updated fields: " + ($changedFields -join ", "))
}

View File

@@ -0,0 +1,71 @@
param(
[string]$RussianPath = "Mods/DnD 5.5e AIO Russian/Localization/Russian/russian.xml",
[string]$EnglishPath = ".cache/upstream/english.xml",
[string]$OutputDir = "build/translation-diff",
[string]$EditsPath = ""
)
$ErrorActionPreference = "Stop"
$getUpstreamScriptPath = Join-Path $PSScriptRoot "get-upstream-english.ps1"
$compareScriptPath = Join-Path $PSScriptRoot "compare-translation.ps1"
$applyScriptPath = Join-Path $PSScriptRoot "apply-translation-edits.ps1"
foreach ($scriptPath in @($getUpstreamScriptPath, $compareScriptPath, $applyScriptPath)) {
if (-not (Test-Path -LiteralPath $scriptPath)) {
throw "Required script was not found: '$scriptPath'."
}
}
$resolvedOutputDir = [System.IO.Path]::GetFullPath($OutputDir)
$resolvedProvidedEditsPath = ""
if (-not [string]::IsNullOrWhiteSpace($EditsPath)) {
$resolvedProvidedEditsPath = [System.IO.Path]::GetFullPath($EditsPath)
}
$workingDiffDir = Join-Path $env:TEMP ("bg3-translation-update-" + [guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $workingDiffDir -Force | Out-Null
try {
& $getUpstreamScriptPath -OutputPath $EnglishPath -Force
& $compareScriptPath -EnglishPath $EnglishPath -RussianPath $RussianPath -OutputDir $workingDiffDir
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
Get-ChildItem -LiteralPath $workingDiffDir | ForEach-Object {
$destinationPath = Join-Path $resolvedOutputDir $_.Name
if ($resolvedProvidedEditsPath -and ([System.IO.Path]::GetFullPath($destinationPath) -eq $resolvedProvidedEditsPath) -and (Test-Path -LiteralPath $resolvedProvidedEditsPath)) {
return
}
Copy-Item -LiteralPath $_.FullName -Destination $resolvedOutputDir -Recurse -Force
}
$summaryJsonPath = Join-Path $workingDiffDir "summary.json"
if (-not (Test-Path -LiteralPath $summaryJsonPath)) {
throw "Translation summary was not found: '$summaryJsonPath'."
}
$summary = Get-Content -LiteralPath $summaryJsonPath -Raw | ConvertFrom-Json -Depth 10
$hasDiff = ($summary.missingInRussianCount -gt 0) -or ($summary.versionMismatchCount -gt 0) -or ($summary.staleOnlyInRussianCount -gt 0)
if (-not $hasDiff) {
Write-Host "[update-translation.ps1] Перевод уже актуален, дополнительные действия не требуются."
return
}
if ([string]::IsNullOrWhiteSpace($EditsPath)) {
Write-Host "[update-translation.ps1] Найдены изменения перевода. Подготовьте правки в '$([System.IO.Path]::GetFullPath((Join-Path $resolvedOutputDir "candidates.json")))' и затем запустите повторно с '-EditsPath'."
return
}
$effectiveEditsPath = [System.IO.Path]::GetFullPath($EditsPath)
if (-not (Test-Path -LiteralPath $effectiveEditsPath)) {
throw "Prepared edits file was not found: '$effectiveEditsPath'."
}
& $applyScriptPath -RussianPath $RussianPath -EditsPath $effectiveEditsPath
Write-Host "[update-translation.ps1] Обновление перевода завершено. Результат записан в '$([System.IO.Path]::GetFullPath($RussianPath))'."
} finally {
if (Test-Path -LiteralPath $workingDiffDir) {
Remove-Item -LiteralPath $workingDiffDir -Recurse -Force
}
}

View File

@@ -0,0 +1,41 @@
param(
[Parameter(Mandatory = $true)]
[string]$XmlPath
)
$ErrorActionPreference = "Stop"
$resolvedXmlPath = [System.IO.Path]::GetFullPath($XmlPath)
if (-not (Test-Path -LiteralPath $resolvedXmlPath)) {
throw "XML file was not found: '$resolvedXmlPath'."
}
[xml]$xml = Get-Content -LiteralPath $resolvedXmlPath -Raw
$contentListNode = $xml.SelectSingleNode('/contentList')
if ($null -eq $contentListNode) {
throw "XML validation failed: missing '/contentList' in '$resolvedXmlPath'."
}
$contentNodes = $xml.SelectNodes('/contentList/content')
if ($null -eq $contentNodes -or $contentNodes.Count -lt 1) {
throw "XML validation failed: no '/contentList/content' entries found in '$resolvedXmlPath'."
}
$seen = [System.Collections.Generic.HashSet[string]]::new()
foreach ($node in $contentNodes) {
$contentUid = [string]$node.GetAttribute("contentuid")
if ([string]::IsNullOrWhiteSpace($contentUid)) {
throw "XML validation failed: found content node without 'contentuid' in '$resolvedXmlPath'."
}
if (-not $seen.Add($contentUid)) {
throw "XML validation failed: duplicate contentuid '$contentUid' in '$resolvedXmlPath'."
}
$version = [string]$node.GetAttribute("version")
if ([string]::IsNullOrWhiteSpace($version)) {
throw "XML validation failed: contentuid '$contentUid' has empty 'version' in '$resolvedXmlPath'."
}
}
Write-Host "[validate-translation-xml.ps1] XML is valid: '$resolvedXmlPath'. Entries=$($contentNodes.Count)."