Compare commits
61 Commits
v0.1.5
...
b684bf195e
| Author | SHA1 | Date | |
|---|---|---|---|
| b684bf195e | |||
| 39ac7a54ce | |||
| 92c78f0813 | |||
| 07c7fc8aa7 | |||
| cb37f422ac | |||
| 72bab59520 | |||
| 1ca6d579da | |||
| 6ede25dc35 | |||
| de85438afe | |||
| 111cf8c269 | |||
| d2299199ba | |||
| ec062817dc | |||
| f56e10748e | |||
| ac1bd03426 | |||
| 07f557ca30 | |||
| 7f189aa741 | |||
| d048a33c55 | |||
| 4e99dfdd92 | |||
| 5d54da9048 | |||
| d96db4e1f0 | |||
| c40712701c | |||
| c9595312ab | |||
| 91e12e4ba1 | |||
| 921a5a3156 | |||
| 2ab32b258c | |||
| 175c1dbaed | |||
| e9bfbfe74f | |||
| 6782cfcd87 | |||
| eaf84ad605 | |||
| 65e3f5b48e | |||
| 9519b92771 | |||
| 6257027a13 | |||
| cde9194ed5 | |||
| 1c8cf13f67 | |||
| 74a8942999 | |||
| 7f8f09a3ac | |||
| 70f93c3d29 | |||
| 4aa2e136b2 | |||
| 3ae30a5263 | |||
| 4da26911fe | |||
| df1daee6ab | |||
| 9d1a26c8e0 | |||
| 8a4970742c | |||
| b50a6a2f95 | |||
| 4646b51459 | |||
| a72b7bc1e1 | |||
| 7aca648396 | |||
| 36129b15d1 | |||
| 97ca95ba16 | |||
| ad129e15d5 | |||
| c8371a3fec | |||
| 321fef2f63 | |||
| eb9ba5eefa | |||
| c0524832d0 | |||
| e78610c702 | |||
| 5c0a44a71e | |||
| c859ddc50c | |||
| 5f3ca9ae0d | |||
| 782879d73e | |||
| d5cd0300da | |||
| 85a7ec546c |
117
.github/workflows/build.yml
vendored
Normal file
117
.github/workflows/build.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
build/
|
build/
|
||||||
build-stage*
|
build-stage*
|
||||||
.tools/
|
.tools/
|
||||||
|
.cache/
|
||||||
*.pak
|
*.pak
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|||||||
137
ACTIONS.md
Normal file
137
ACTIONS.md
Normal 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
|
||||||
|
|
||||||
279
AGENTS.md
279
AGENTS.md
@@ -1,153 +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
|
||||||
- Glossary: `glossary/glossary.normalized.json`
|
2. merge → main + delete branch
|
||||||
- Mod metadata: `Mods/DnD 5.5e AIO Russian/meta.lsx`
|
|
||||||
- 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.
|
|
||||||
|
|||||||
BIN
Mods/DnD 5.5e AIO Russian/GUI/metadata.lsf
Normal file
BIN
Mods/DnD 5.5e AIO Russian/GUI/metadata.lsf
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
BIN
Mods/DnD 5.5e AIO Russian/mod_publish_logo.png
Normal file
BIN
Mods/DnD 5.5e AIO Russian/mod_publish_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -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**.
|
||||||
|
|
||||||
Этот проект предназначен для русской локализации оригинального мода и его текстового контента.
|
Локализация поддерживается в темпе с апстримом, который сейчас развивается в сторону более полного охвата классов, рас, предысторий, фитов и заклинаний.
|
||||||
|
|||||||
@@ -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": "Серп",
|
||||||
|
|||||||
210
scripts/apply-translation-edits.ps1
Normal file
210
scripts/apply-translation-edits.ps1
Normal 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 ', ')"
|
||||||
|
}
|
||||||
@@ -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 },
|
||||||
|
|||||||
209
scripts/compare-translation.ps1
Normal file
209
scripts/compare-translation.ps1
Normal 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] Перевод уже актуален, дополнительные действия не требуются."
|
||||||
|
}
|
||||||
44
scripts/get-upstream-english.ps1
Normal file
44
scripts/get-upstream-english.ps1
Normal 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
63
scripts/set-version.ps1
Normal 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')."
|
||||||
91
scripts/sync-parent-meta.ps1
Normal file
91
scripts/sync-parent-meta.ps1
Normal 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 ", "))
|
||||||
|
}
|
||||||
71
scripts/update-translation.ps1
Normal file
71
scripts/update-translation.ps1
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
41
scripts/validate-translation-xml.ps1
Normal file
41
scripts/validate-translation-xml.ps1
Normal 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)."
|
||||||
Reference in New Issue
Block a user