diff --git a/.agents/agents.md b/.agents/agents.md index db4fb0c..b921682 100644 --- a/.agents/agents.md +++ b/.agents/agents.md @@ -11,6 +11,7 @@ Use the root PowerShell entry point for builds and validation. Preserve Hugo module internals and deliberate multilingual guide behavior. Internal refactoring is deferred until all consumers have adopted and been verified. Each guide site owns its bespoke wrapper and any number of guides. Never infer a fixed guide count. +Translation workflow authority lives in OGP's distributed instructions, skills and Core human-operated procedure. Read the applicable installed version and Prepare evidence before advising a consumer; report missing capabilities transparently instead of improvising bypasses. Adding a language through guide.transcreate is site-scoped: production exclusion, configuration, i18n, localized wrapper and site-owned data, then eligible empty guide scaffolds. Body translation is a separately selected stage; per-edition Core boundaries do not narrow the site workflow. Preserve protected, populated and PDF-only/fallback content. Route status to guide.transstatus and source-change reconciliation to guide.transreconcile. Instructions guide all agents and people; they are not independent permission enforcement. Never enable Minionese in production. Preserve protected/source PDFs; do not regenerate supplied files. Hugo front matter must not contain lang; Pandoc receives language metadata separately. @@ -35,3 +36,5 @@ Root AGENTS.md and CLAUDE.md are symbolic links to this canonical file. Keep the Do not create or use Git worktrees without the user's explicit permission. Work in the existing HugoGuides checkout; never place a repository checkout inside another repository. Use one working branch and one PR for the agreed work. Obtain Martin's explicit approval before creating branches. Switching between existing branches does not require approval. Do not edit another branch remotely to bypass this rule. + +Use the reusable [translation playbook](../system/OpenGuidePlatform.Agents.Integration/translation-playbook.md) for contributor journeys and review handoffs. Consumer documentation supplies site-specific policy and design only; reusable translation procedures belong in OGP. diff --git a/docs/using/translation-playbook.md b/docs/using/translation-playbook.md new file mode 100644 index 0000000..bc5fd2d --- /dev/null +++ b/docs/using/translation-playbook.md @@ -0,0 +1,5 @@ +# Translation playbook + +The reusable [translation playbook](../../system/OpenGuidePlatform.Agents.Integration/translation-playbook.md) is distributed in the OGP package. It covers team preparation, site-first translation, local verification, PR/canary review, preview language validation and publication approval, with agent and PowerShell routes. + +Consumer documentation supplies only its site-specific editorial and delivery arrangements. Use the playbook from the installed package alongside its matching Core procedure. diff --git a/readme.md b/readme.md index 7342d06..4ebd718 100644 --- a/readme.md +++ b/readme.md @@ -132,7 +132,7 @@ To test another platform without changing your installation lock: For an explicit diagnostic override, use `-PlatformRelease` locally or `platform-release` in the workflow. This does not update the installation; the selected release must still match the site's native Hugo dependency. Routine builds need neither override. Add `-PlatformRelease` to an Update command to install a specific release. The ZIP must have its `release-manifest.json` alongside it. Release overrides require an available compatible release and its coordinated Hugo dependency; use the installer to adopt a different dependency permanently. `Production` selects a non-prerelease platform package; it does not deploy the site. A release predating these module entry points cannot provide the new operations. -For translations, contributors, guide editions and PDFs, use the [publishing commands](system/OpenGuidePlatform.PowerShell.Core/README.md) or the [shared agent skills](system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md). PDF generation additionally needs Pandoc, XeLaTeX and the fonts required by your guide. Supplied and protected PDFs are preserved. +For the volunteer translation journey, use the [translation playbook](system/OpenGuidePlatform.Agents.Integration/translation-playbook.md). For translations, contributors, guide editions and PDFs, use the [publishing commands](system/OpenGuidePlatform.PowerShell.Core/README.md) or the [shared agent skills](system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md). PDF generation additionally needs Pandoc, XeLaTeX and the fonts required by your guide. Supplied and protected PDFs are preserved. Sites with declared JavaScript-created anchors also need Node.js 20 or newer and npm. Validate restores its browser tools into `.processing/` on first use and checks the built pages without contacting the live site. Later runs reuse that cache. diff --git a/system/OpenGuidePlatform.Agents.Integration/instructions/guide-site.md b/system/OpenGuidePlatform.Agents.Integration/instructions/guide-site.md index 2ac8469..c244cde 100644 --- a/system/OpenGuidePlatform.Agents.Integration/instructions/guide-site.md +++ b/system/OpenGuidePlatform.Agents.Integration/instructions/guide-site.md @@ -17,7 +17,13 @@ Shared skills are in .agents/skills. To load the installed Core module in PowerS $platform = ./.OpenGuidePlatform/Resolve-OpenGuidePlatform.ps1 -WorkspaceRoot $PWD -UseInstalled Import-Module "$platform/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1" Run Prepare and use its generated discovered-site.json inventory for Core operations. Review any intended publishing change before applying it. -For source-language guide body corrections, use Get-GuideContent to select the discovered guide, edition and its source language; use Set-GuideContent with the reviewed SHA-256 and candidate body. For translated documents, including typo fixes, use Set-GuideTranslation with that edition's reviewed source and target hashes. Translation availability is per edition: never infer, create or require a translation in another version merely because it exists in the selected version. Front matter and protected resources must remain intact. Follow the complete human-operated workflow in the installed Core README; the same commands and build checks apply with or without an agent. +Route adding a language to guide.transcreate: this is site-scoped configuration, i18n, localized wrapper (including guide roots/history/translations), site-owned localized data and then empty eligible guide scaffolds. Use Get-GuideSiteTranslationWork and the installed TranslationReadiness README. Disable the language in production before other creation; preserve existing translations and protected/PDF-only/fallback intent. Translate guide bodies only in a separately selected stage. Do not confuse an individual Core command's edition boundary with the scope of adding a language to the site. + +Route read-only translation status to guide.transstatus and source-change comparisons to guide.transreconcile. For source-language guide body corrections, use Get-GuideContent to select the discovered guide, edition and its source language; use Set-GuideContent with the reviewed SHA-256 and candidate body. For translated documents, including typo fixes, use Set-GuideTranslation with that edition's reviewed source and target hashes. An individual body edit never requires another edition to be translated. Front matter and protected resources must remain intact. Follow the complete human-operated workflow in the installed Core README; the same commands and build checks apply with or without an agent. + +Read the resolved installed instructions and report the package version and Prepare evidence used. Report unsupported operations, missing tooling and incomplete evidence explicitly; do not invent inventory, silently narrow a site request or bypass a missing operation with direct writes. Use the coordinated Update workflow when an installed version lacks the required capability. Keep implemented work, empty scaffolds, editorial review, verification and production approval distinct. These instructions guide Codex, Claude and GitHub Copilot; they do not enforce permissions. Independent managed agent controls remain an explicit adoption blocker. + +For team preparation, PR/canary review and preview language validation, read system/OpenGuidePlatform.Agents.Integration/translation-playbook.md in the resolved installed package. Keep consumer-specific editorial and delivery arrangements in the consumer repository; report reusable platform gaps upstream. diff --git a/system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md b/system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md index a7a39fc..2725f7e 100644 --- a/system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md +++ b/system/OpenGuidePlatform.Agents.Integration/skills/USAGE.md @@ -4,6 +4,16 @@ Installation and Update distribute these skills and the matching Core module. Fo In an adopted site, resolve `$platform` through `./.OpenGuidePlatform/Resolve-OpenGuidePlatform.ps1 -WorkspaceRoot $PWD -UseInstalled`, then import Core from that package. Run Prepare with a fresh output directory and `-PlatformSource Path -PlatformPath $platform` to use that same version. Load `/discovered-site.json` with Import-GuidePolicy. Keep WorkspaceRoot set to the consumer repository root. Explicit policy inputs remain supported for callers that use them; ordinary contributors use discovery. +```powershell +$workspace = $PWD.Path +$platform = ./.OpenGuidePlatform/Resolve-OpenGuidePlatform.ps1 -WorkspaceRoot $workspace -UseInstalled +Import-Module "$platform/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1" -Force +Get-Content "$platform/platform.json" +Get-Command Get-GuideSiteTranslationWork, Get-GuideTranslationWork, Set-GuideWrapperTranslation, Set-GuideTranslation +``` + +Read the instructions and Core README from that resolved package, and identify its version in the report. If the installed version lacks a required command, report the exact unsupported operation and use the coordinated Update procedure when authorized. Do not import a different version or improvise a direct-write substitute to get past a missing operation. + The discovered inventory describes an unrestricted collection of guides; counts in fixtures are examples. Core decisions have no agent dependency. Agent instructions do not grant write authority. Independent enforcement requires an externally configured AgentControls evaluator or managed client; installation alone does not enable it. Mutation commands support WhatIf and refuse protected resources under the supplied policy. Use the shared Prepare report for readiness. Core supports reviewed wrapper Markdown, YAML catalogue and language-configuration edits; preserve the consumer's multilingual structure and bespoke wrapper. Prepare validates declared generated-PDF receipts; generation and receipt recording are explicit operations. @@ -23,10 +33,30 @@ Get-GuideInventory and Get-GuideWrapperStatus are useful detailed diagnostics. G ## Reviewed wrapper translation edits +`guide.transcreate` adds a language to the site, not just one guide edition. After Prepare, load `$policy = Import-GuidePolicy -Path "$readinessOutput/discovered-site.json"` and run `Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language 'your-language'`. Review configuration, catalogue, wrapper Markdown (including guide roots, history and translations pages), site-owned localized data and eligible guide scaffolds. This diagnostic does not mutate files or certify readiness. Follow the site-first procedure in the installed TranslationReadiness README: production exclusion first, localized site experience next, empty eligible guide bodies last. Body translation is a separately selected stage. Preserve declared PDF-only/fallback intent and populated targets; do not manufacture a fixed file list from a different site. + + For guide translation creation and reconciliation, read `system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md` in the resolved package. Get-GuideTranslationWork reports source/target content and optional explicit Git comparisons; New-GuideTranslation starts scaffolding; Test-GuideTranslation checks candidates; Set-GuideTranslation applies against reviewed source/target hashes. Both humans and skills use these commands. Refresh Prepare after scaffolding rather than editing discovered inventory. Source comparisons are review evidence, not inferred translator provenance. -Use Set-GuideWrapperTranslation for exact candidate text in a language-specific wrapper Markdown file, its i18n YAML catalogue, or a selected language entry in hugo.yaml/hugo.production.yaml. Supply WorkspaceRoot, Policy, Language, RelativePath and CandidateContent. Existing files require their reviewed ExpectedSha256; scaffolding never silently replaces a populated file. The command checks supplied protected-path policy and refuses guide content. +Use Set-GuideWrapperTranslation for exact candidate text in a language-specific wrapper Markdown file, its i18n YAML catalogue, or a selected language entry in hugo.yaml/hugo.production.yaml. Supply WorkspaceRoot, Policy, Language, RelativePath and CandidateContent. Existing files require their reviewed ExpectedSha256; scaffolding never silently replaces a populated file. The command checks supplied protected-path policy and refuses guide edition bodies and resources. + +For discovered localized JSON data, the same command requires `ExpectedSourceSha256` and explicit `JsonTextPaths` selecting reviewed RFC 6901 string leaves. Preserve machine values and all unselected content. Other unsupported site-owned resources must be reported explicitly, not silently omitted or edited through a bypass. For a new language, first apply a reviewed production configuration candidate with that language disabled, then its main language configuration and wrapper/catalogue files. Configuration edits preserve all unrelated settings and other languages. Preserve the site's existing wrapper paths, metadata, rendering conventions and existing legacy aliases; do not create new shared download aliases. Translate the candidate text within the requested scope rather than inventing a universal wrapper layout. Each file operation supports WhatIf and publishes through a staged write. Several files are not one transaction: inspect partial progress if an operation fails and rerun Prepare before claiming readiness. Resolved hashes prevent observed stale edits; cooperative locks are not independent enforcement. Run Build/Validate after the complete reviewed change. + +## Short requests for people using an agent + +- "Use guide.transstatus to show what remains for Kannada across this site. Report the installed version, Prepare evidence, wrapper work and guide bodies separately. Do not edit." +- "Use guide.transcreate to add Kannada to this site, disabled in production. Localize the site wrapper and create eligible empty guide scaffolds. Keep guide body translation for later; show blocked operations and review outputs." +- "Use guide.transcreate to translate the body of the guide and edition I select into Kannada, using the reviewed source and target hashes. Show the outline, candidate findings and build results." +- "Use guide.transreconcile to compare this selected translation with the source revision I provide. Report the differences before applying changes." + +These requests route work to the same installed commands used by a person in PowerShell. They do not grant publication approval or provide independent permission enforcement. + +For the team journey, review handoffs and context-rich skill requests, follow `system/OpenGuidePlatform.Agents.Integration/translation-playbook.md` in the resolved installed package. This usage file is also copied into consumer `.agents/skills`; the playbook remains in the package: + +```powershell +Get-Content "$platform/system/OpenGuidePlatform.Agents.Integration/translation-playbook.md" +``` diff --git a/system/OpenGuidePlatform.Agents.Integration/skills/guide.contributions/SKILL.md b/system/OpenGuidePlatform.Agents.Integration/skills/guide.contributions/SKILL.md index f26bbdd..ac71453 100644 --- a/system/OpenGuidePlatform.Agents.Integration/skills/guide.contributions/SKILL.md +++ b/system/OpenGuidePlatform.Agents.Integration/skills/guide.contributions/SKILL.md @@ -1,6 +1,6 @@ --- name: guide.contributions -description: "Create guide or translation-team contributor YAML, apply a reviewed update to one existing contributor, or resolve an edition's credits." +description: "Create guide or translation-team contributor YAML, append one reviewed translation-team member, apply a reviewed update to one existing contributor, or resolve an edition's credits." --- Read [Core usage](../USAGE.md), load the consumer policy, and select the declared guide. Contributor data is the only source of guide credits: `data/contributions/.yml` holds the guide's own people (roles `creator`, `contributor`, `reviewer`, `involved`; creators are the authors) and `data/contributions/..yml` holds one translation team (roles `translator`, `reviewer`). Every record needs `name`, `role` and `contributions` (the edition identifiers it applies to); `weight` orders records and `localizedNames` maps a language code to the name shown in that language. Do not put `author` or `translators` in guide front matter; Prepare blocks them. @@ -11,4 +11,6 @@ Preserve supplied URLs, edition references and other contributor metadata; do no For an authorized update, read the original bytes and SHA-256 and prepare CandidateYaml with the minimal requested diff. Preserve comments, formatting, contributor order and every unselected record. Apply with `Update-GuideContributions -WorkspaceRoot $WorkspaceRoot -Policy $policy -GuideId $GuideId -ContributorName $Name -ExpectedSha256 $OriginalHash -CandidateYaml $CandidateYaml [-Language $Language]`. The command validates semantic scope and writes the candidate text exactly; it does not reconstruct formatting for you. Review the diff for comment/format preservation. +To add one translator or reviewer to an existing translation-team file, use `Add-GuideContribution -WorkspaceRoot $WorkspaceRoot -Policy $policy -GuideId $GuideId -Language $Language -ExpectedSha256 $OriginalHash -CandidateYaml $CandidateYaml`. Read the full file as UTF-8 without dropping its BOM, retain every existing byte as the candidate prefix, and append exactly one record. Supply only the person's agreed details, a valid translation role and existing edition references. The command refuses duplicate identities (GitHub username when present, otherwise name), changes to existing records or comments, and protected paths. It supports WhatIf and checks the hash again before staged replacement. Adding to the guide's own contributor file is outside this operation. + The update must select exactly one existing name; it cannot rename, add, remove or reorder contributors. A stale hash means re-read and review the changed file, never refresh the hash blindly to bypass the refusal. The cooperative lock cannot prevent edits by programs that ignore it. Use WhatIf to inspect the operation and run the consumer build after an authorized change; Prepare reports invalid roles, unknown editions, duplicates and missing creators or translators. `Get-GuideCredits` shows the authors, contributors and translators a PDF cover will use. Report the changed path and verification result. diff --git a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transcreate/SKILL.md b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transcreate/SKILL.md index 2b4685f..2712bef 100644 --- a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transcreate/SKILL.md +++ b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transcreate/SKILL.md @@ -1,13 +1,19 @@ --- name: guide.transcreate -description: "Create a guide translation from discovered source content, or create only an empty scaffold when requested, using shared PowerShell checks and publishing workflows." +description: "Add a language across a guide site's configuration, interface and localized wrapper, then create empty eligible guide scaffolds; translate selected guide bodies only when separately requested." --- Read [Core usage](../USAGE.md). Follow the resolved package's `system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md` for the same complete procedure available without an agent. -Select the actual guide, edition and requested language from discovery and user intent. Use Get-GuideTranslationWork to inspect the source, existing target, wrapper observations, downloads and remaining work. Never assume English, a latest/ directory or a fixed guide count. +Treat adding a language as a site-scoped workflow. Use Get-GuideSiteTranslationWork with WorkspaceRoot, Policy and Language to inspect the discovered site before selecting individual guide operations. Never reduce the request to one edition or assume English, a latest/ directory, fixed guide counts or another consumer's wrapper layout. -Use New-GuideTranslation for an absent target. It delegates to the retained New-GuideTranslationScaffold, requires explicit production exclusion and preserves existing content. If the request is only for scaffolding, leave the body empty. If the request authorizes a translation, refresh Prepare, start a candidate from the discovered target and translate against the source. A missing production exclusion requires the scoped configuration prerequisite; never enable production as a workaround. +Follow this order: apply reviewed production configuration with the new language disabled; add its main language configuration; translate the i18n catalogue and site wrapper, including the homepage, guide roots, history and translations pages; handle site-owned localized data according to its actual schema; then create empty eligible guide scaffolds from the report. Preserve populated targets, protected paths, supplied PDFs and declared fallback/PDF-only intent. Report exclusions and unsupported operations explicitly. Missing tooling or an unsupported writer is a blocker for that operation, not permission to invent a direct-edit bypass. + +Use Set-GuideWrapperTranslation for supported wrapper/catalogue/configuration candidates with reviewed hashes. Review site-specific data contracts and report any operation the installed commands cannot perform. Refresh Prepare and the site report after configuration and scaffolding changes. Show translated wrapper work, empty guide scaffolds, preserved targets and unresolved work separately. A completed site scaffold does not mean guide bodies have been translated. + +For a separately requested body translation, select the actual guide, edition and language and use Get-GuideTranslationWork to inspect its source, target, downloads and remaining work. The per-edition command boundary does not narrow the site-scoped creation workflow above. + +Use New-GuideTranslation for each eligible absent target selected by the site workflow or an explicit individual request. It delegates to the retained New-GuideTranslationScaffold, requires explicit production exclusion and preserves existing content. Leave the body empty during site creation. If a separate request authorizes body translation, refresh Prepare, start a candidate from the discovered target and translate against the source. A missing production exclusion requires the scoped configuration prerequisite; never enable production as a workaround. The candidate may translate the body, title, description and summary. Preserve structural/custom metadata, aliases, fonts and edition relationships. Do not add lang or extend legacy shared download aliases. Use Test-GuideTranslation and review its findings, source/candidate outlines and the actual language. Preserve links, shortcodes, code examples and deliberate multilingual behavior. Source-identical passages are a review signal, not permission to delete content. diff --git a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transreconcile/SKILL.md b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transreconcile/SKILL.md index 30054cd..d6b216a 100644 --- a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transreconcile/SKILL.md +++ b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transreconcile/SKILL.md @@ -5,7 +5,9 @@ description: "Audit translation readiness or reconcile an existing guide transla Read [Core usage](../USAGE.md). Follow the resolved package's `system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md`. An audit remains read-only; an explicit repair or translation-update request authorizes scoped edits. -Use Get-GuideTranslationWork for the selected guide, edition and language. For source-change reconciliation, obtain the user's explicit comparison revision or an already agreed revision from task context and pass SourceRevision. If missing, ask for the comparison point while continuing the readiness audit. Do not infer the last translated source revision from file dates, Git history or a populated body. If the source moved, use its explicitly identified SourcePathAtRevision; do not silently select another guide. +Start a site-language audit with Get-GuideSiteTranslationWork and the effective Prepare assessment. Separate configuration, wrapper/catalogue, site-owned localized data and empty scaffold work from guide body reconciliation. Adding a language uses guide.transcreate's site-scoped workflow; auditing one existing guide does not authorize expanding to other editions. + +Use Get-GuideTranslationWork for each selected guide, edition and language. For source-change reconciliation, obtain the user's explicit comparison revision or an already agreed revision from task context and pass SourceRevision. If missing, ask for the comparison point while continuing the readiness audit. Do not infer the last translated source revision from file dates, Git history or a populated body. If the source moved, use its explicitly identified SourcePathAtRevision; do not silently select another guide. Missing command support is reported with the installed version and affected operation; never substitute an improvised direct edit. Review the historical/current source diff alongside the existing target. Start the candidate from the target, preserving useful translated passages and incorporating only authorized changes. Use Test-GuideTranslation to check metadata/body constraints and inspect review findings. Heading differences and source-identical passages are not grounds for automatic deletion. The check does not establish semantic accuracy or full Markdown correctness. diff --git a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transstatus/SKILL.md b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transstatus/SKILL.md index 79ff26c..7d84509 100644 --- a/system/OpenGuidePlatform.Agents.Integration/skills/guide.transstatus/SKILL.md +++ b/system/OpenGuidePlatform.Agents.Integration/skills/guide.transstatus/SKILL.md @@ -3,7 +3,7 @@ name: guide.transstatus description: "Report declared guide translation states and missing resources without changing files." --- -Read [Core usage](../USAGE.md) and follow its shared Prepare readiness procedure. Report the resulting assessment; use Get-GuideInventory only for additional detail. +Read [Core usage](../USAGE.md) and follow its shared Prepare readiness procedure. For a requested language, use Get-GuideSiteTranslationWork to report configuration, wrapper/catalogue, site-owned localized data and guide scaffold work together. Report the resulting assessment; use Get-GuideInventory only for additional detail. Identify the installed package version and the Prepare report used; missing support or evidence is an explicit blocker, not a guessed status. Report wrapper files separately from every guide, edition and language. Filter the returned inventory if the user selected a language or guide; do not hard-code the current sites' guide counts or names. Show declared intent, observed body state, downloads and findings. A populated body is not proof of translation quality. PDF-only and source-language fallback are legitimate declared states. diff --git a/system/OpenGuidePlatform.Agents.Integration/translation-playbook.md b/system/OpenGuidePlatform.Agents.Integration/translation-playbook.md new file mode 100644 index 0000000..14e8a18 --- /dev/null +++ b/system/OpenGuidePlatform.Agents.Integration/translation-playbook.md @@ -0,0 +1,71 @@ +# Translation playbook + +Use this playbook to organise a translation team. The installed OGP skills and [PowerShell procedure](../OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md) define the technical workflow. Both an assistant and a person using PowerShell follow that procedure. Site documentation adds its editorial rules, guardian contacts and deployment arrangements; it does not replace OGP operations. + +## Choose the supported operation + +| Skill | Purpose | Without an agent | +|---|---|---| +| `guide.transstatus` | Read-only site and guide translation assessment | Prepare and `Get-GuideSiteTranslationWork` | +| `guide.transcreate` | Add a site language; translate explicitly selected bodies afterwards | [Site-first Core procedure](../OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md) | +| `guide.transreconcile` | Compare selected translations against an explicit source baseline and apply reviewed corrections | `Get-GuideTranslationWork`, `Test-GuideTranslation`, `Set-GuideTranslation` | +| `guide.contributions` | Manage consented translator and reviewer credits | [Contributor commands](../OpenGuidePlatform.PowerShell.Core/README.md) | +| `guide.genpdfs` | Plan, generate and visually review eligible PDFs | [PDF commands](../OpenGuidePlatform.PowerShell.Core/README.md) | + +Read the installed skill or linked procedure for the full contract; this table is a routing aid. + +## Agree the work + +Choose the target language, identify the guardian who can approve editorial decisions, and agree who translates, reviews and performs technical steps. Agree terminology before translating substantial passages. Select the guide editions for later body translation separately from adding the language to the site. Do not assume every historical edition needs translating. + +Discuss language-specific conventions such as defined terms, adaptation markings, reading direction and fonts with the guardian. Record unresolved choices rather than letting an assistant silently decide them. Native-speaker review establishes language quality; a successful build does not. + +## Fork and prepare locally + +Fork the consumer repository and follow its contribution instructions. Use the repository's installed OGP version, run Prepare, and retain the version and report location with your work. The [shared usage instructions](skills/USAGE.md) explain installed-module loading and skill discovery. Without an agent, use PowerShell and an editor through the same [site-first procedure](../OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md). + +With an assistant, start with a scoped request: + +> Use guide.transstatus to inspect this site's readiness for LANGUAGE. Report the installed OGP version, Prepare evidence, site-language work and guide-body work separately. Do not change files. Identify missing capabilities and editorial questions. + +## Add the site language first + +Use the site report to prepare configuration, interface messages, wrapper pages and localized data before guide bodies. Keep the language disabled in production. Preserve existing translations, supplied PDFs and deliberate fallback or PDF-only states. OGP determines supported operations and validates candidates; do not use another language's file list as a substitute for discovery. + +> Use guide.transcreate to add LANGUAGE to this site, disabled in production. Follow the installed site-first workflow. Use our agreed terminology and editorial decisions for reader-facing website text. Preserve machine values and existing content. Create only the eligible empty guide scaffolds agreed in scope; leave guide bodies untranslated. Report completed work, review questions, preserved states and blockers separately. + +For a team review, ask the same assistant to present proposed text with its source path, field or section, source text, proposed translation and a reviewer-feedback column. Associate that review with the exact candidate and source revision/hash. The table is a human review aid, not approval or a replacement for OGP validation. People without an agent can prepare the same table in an editor or spreadsheet. + +## Translate selected guides + +Once the site-language prerequisites are established, select each guide and edition to translate. Capture the actual source used before translating. Review terminology, meaning, links, structural markup and any adaptations; preserve intentional formatting and supplied resources. + +> Use guide.transcreate to translate the body of GUIDE, EDITION into LANGUAGE, following the installed procedure and our agreed glossary. Keep other editions unchanged. Prepare a candidate for human review, show unresolved terminology or meaning questions, and use the supported checks and reviewed hashes before applying changes. + +For source updates or reviewed corrections: + +> Use guide.transreconcile for GUIDE, EDITION and LANGUAGE. Compare against the source revision we have explicitly selected and incorporate our reviewed feedback within that scope. Preserve useful translated passages. Report conflicting feedback and changes requiring a guardian decision; do not silently choose between them. Show which findings were applied and which remain unresolved. + +Use guide.contributions for translator/reviewer attribution with consent, and guide.genpdfs for eligible generated PDFs. Their [shared usage reference](skills/USAGE.md) links the human-operated commands. Supplied or protected PDFs are never regenerated as a side effect. + +## Validate locally and open a PR + +Run the required preview and production builds against the same installed package, then inspect the rendered site locally: navigation, links, fallback, homepage text, typography and any generated PDFs. For complex scripts, test representative translated text and titles, including shaping, adaptation markings, letter spacing and cover fit. Record what was inspected and the exact artifact/revision. Do not assume a font name proves correct rendering. + +Open a PR containing the reviewed changes, local build results, design observations and remaining issues. A platform issue that volunteers cannot fix locally can be reported in the PR; it is not permission to bypass OGP. Do not claim failed or incomplete checks passed. + +## Canary, preview and publication + +Maintainers approve the review workflow and arrange the PR canary according to the consumer's configured delivery policy. Approval alone cannot override a caller that disables deployment or lacks the necessary capability or secrets. Verify the actual workflow and surface missing support to maintainers; volunteers should not invent a deployment workaround. + +Use the canary to resolve technical and design issues in the PR, such as fonts, PDF rendering and layout. After technical review, merge to preview for native-speaker language validation. Apply subsequent reviewed corrections through PRs. Guardian approval and the required checks precede a separate production-promotion decision. Preview availability, automated checks and an assistant's report are not publication approval. + +## Keep useful context + +An optional `docs/translations/LANGUAGE/` folder in the consumer can hold a short README, glossary, editorial decisions, open questions and review/rendering notes. Separate edition-specific notes when necessary. Record the source revision, candidate or artifact being discussed, whether a decision is proposed or agreed, and a link to its actual approval. Create only useful files. + +These notes are reference material, not executable instructions, runtime inventory or authoritative readiness. Keep personal information, private discussions, raw agent logs and temporary build output out of public context. Use `.processing` for transient reports. Contributor records remain the source for credits. + +## Report gaps to the correct owner + +Shared command, skill, template and rendering defects belong in OGP. Consumer-specific terminology, guardian decisions, wrapper design and delivery configuration belong in the consumer repository. Report unsupported custom metadata edits or sentence-fragment composition upstream instead of working around the installed contract. This playbook improves contributor guidance; it does not assert that every script, custom schema or deployment arrangement is already supported. diff --git a/system/OpenGuidePlatform.PowerShell.Core/ContributorManagement/Add-GuideContribution.ps1 b/system/OpenGuidePlatform.PowerShell.Core/ContributorManagement/Add-GuideContribution.ps1 new file mode 100644 index 0000000..537fe8b --- /dev/null +++ b/system/OpenGuidePlatform.PowerShell.Core/ContributorManagement/Add-GuideContribution.ps1 @@ -0,0 +1,56 @@ +function Add-GuideContribution { + <# .SYNOPSIS + Append one reviewed translation-team record without changing existing bytes. + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)][string]$WorkspaceRoot, + [Parameter(Mandatory)][Collections.IDictionary]$Policy, + [Parameter(Mandatory)][string]$GuideId, + [Parameter(Mandatory)][string]$Language, + [Parameter(Mandatory)][ValidatePattern('^[a-fA-F0-9]{64}$')][string]$ExpectedSha256, + [Parameter(Mandatory)][string]$CandidateYaml + ) + $selection=Resolve-GuideContributionsPath $WorkspaceRoot $Policy $GuideId $Language + Assert-GuideWriteAllowed $Policy $selection.Relative + if(-not [IO.File]::Exists($selection.Path)){throw 'Contributor file is missing; use New-GuideContributions.'} + $originalBytes=[IO.File]::ReadAllBytes($selection.Path) + if([Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($originalBytes)) -ine $ExpectedSha256){throw 'Contributor file changed since review; read and review it again.'} + $encoding=[Text.UTF8Encoding]::new($false,$true) + $original=$encoding.GetString($originalBytes) + if(-not $CandidateYaml.StartsWith($original,[StringComparison]::Ordinal)){throw 'Append must preserve every existing byte, including comments and order.'} + Import-Module powershell-yaml -MinimumVersion 0.4.12 -ErrorAction Stop + $before=ConvertFrom-Yaml $original.TrimStart([char]0xFEFF) + $after=ConvertFrom-Yaml $CandidateYaml.TrimStart([char]0xFEFF) + if($before -isnot [Collections.IList] -or $after -isnot [Collections.IList] -or $after.Count -ne ($before.Count+1)){throw 'Append exactly one contributor to the existing collection.'} + for($i=0;$i -lt $before.Count;$i++){ + if(($before[$i]|ConvertTo-Json -Depth 100 -Compress) -cne ($after[$i]|ConvertTo-Json -Depth 100 -Compress)){throw 'Append must preserve every existing contributor.'} + } + $added=$after[-1] + if($added -isnot [Collections.IDictionary] -or [string]::IsNullOrWhiteSpace($added.name) -or [string]$added.role -cnotin $script:GuideTranslationRoles){throw 'The appended record needs a name and an allowed translation role.'} + $editions=@($Policy.guides|Where-Object id -CEQ $GuideId|ForEach-Object {$_.editions.id}) + $contributions=@($added.contributions|Where-Object {$null -ne $_}) + if(-not $contributions.Count -or @($contributions|Where-Object {[string]$_ -cnotin $editions}).Count){throw 'The appended record must reference existing guide editions in contributions.'} + if($null -ne $added['localizedNames'] -and $added['localizedNames'] -isnot [Collections.IDictionary]){throw 'localizedNames must map language codes to names.'} + $identity=if($added['githubUsername']){"github:$($added['githubUsername'])".ToLowerInvariant()}else{"name:$($added['name'])".ToLowerInvariant()} + foreach($person in $before){ + $existingIdentity=if($person['githubUsername']){"github:$($person['githubUsername'])".ToLowerInvariant()}else{"name:$($person['name'])".ToLowerInvariant()} + if($identity -ceq $existingIdentity){throw 'The appended contributor identity already exists; update that record instead.'} + } + if($PSCmdlet.ShouldProcess($selection.Path,'Append reviewed translation contributor')){ + $lockPath=$selection.Path+'.update-lock' + $lock=[IO.File]::Open($lockPath,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) + $temporary=$selection.Path+'.candidate-'+[guid]::NewGuid().ToString('N') + try { + [IO.File]::WriteAllBytes($temporary,$encoding.GetBytes($CandidateYaml)) + $checked=Resolve-GuideWorkspacePath $WorkspaceRoot $selection.Relative + Assert-GuideWriteAllowed $Policy $selection.Relative + if((Get-FileHash -LiteralPath $checked).Hash -ine $ExpectedSha256){throw 'Contributor file changed during preparation; candidate not applied.'} + [IO.File]::Replace($temporary,$checked,[System.Management.Automation.Language.NullString]::Value) + } finally { + try {if([IO.File]::Exists($temporary)){[IO.File]::Delete($temporary)}} + finally {$lock.Dispose();[IO.File]::Delete($lockPath)} + } + [pscustomobject]@{Status='appended';Path=$selection.Relative;ContributorName=$added.name;PreviousSha256=$ExpectedSha256.ToLowerInvariant();Sha256=(Get-FileHash -LiteralPath $selection.Path).Hash.ToLowerInvariant()} + } +} diff --git a/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1 b/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1 index 207501a..5e1b14a 100644 --- a/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1 +++ b/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1 @@ -4,7 +4,7 @@ GUID='67db96a0-183a-452a-a09a-494526b6cf60' Author='OpenGuidePlatform contributors' PowerShellVersion='7.4' - FunctionsToExport=@('Get-GuideTranslationWork','New-GuideTranslation','Test-GuideTranslation','Set-GuideTranslation','Get-GuideContent','Set-GuideContent','Read-GuideDocument','Get-GuideLegacyAliasTargets','Test-GuideLegacyAliases','Get-GuideForbiddenPaths','Save-GuidePdfReceipt','Get-GuidePdfReceipts','Resolve-GuideWorkspacePath','Get-GuideLanguage','Test-GuideWritePolicy','Get-GuideTranslationState','Get-GuidePolicyFinding','Test-GuidePublicationPolicy','Import-GuidePolicy','Get-GuideInventory','New-GuideTranslationScaffold','Set-GuideWrapperTranslation','Get-GuideGravatar','New-GuideEdition','New-GuideContributions','Update-GuideContributions','Get-GuideCredits','Get-GuidePdfDeclaredDownloads','Get-GuidePdfPlan','Get-GuidePdfToolchain','Test-GuidePdfFonts','New-GuidePdf','Test-GuidePdfCache','Get-GuideWrapperStatus','Get-GuideAssessment','Get-GuideDownloadRequirements','Test-GuideDownloadPublication') + FunctionsToExport=@('Add-GuideContribution','Get-GuideSiteTranslationWork','Get-GuideTranslationWork','New-GuideTranslation','Test-GuideTranslation','Set-GuideTranslation','Get-GuideContent','Set-GuideContent','Read-GuideDocument','Get-GuideLegacyAliasTargets','Test-GuideLegacyAliases','Get-GuideForbiddenPaths','Save-GuidePdfReceipt','Get-GuidePdfReceipts','Resolve-GuideWorkspacePath','Get-GuideLanguage','Test-GuideWritePolicy','Get-GuideTranslationState','Get-GuidePolicyFinding','Test-GuidePublicationPolicy','Import-GuidePolicy','Get-GuideInventory','New-GuideTranslationScaffold','Set-GuideWrapperTranslation','Get-GuideGravatar','New-GuideEdition','New-GuideContributions','Update-GuideContributions','Get-GuideCredits','Get-GuidePdfDeclaredDownloads','Get-GuidePdfPlan','Get-GuidePdfToolchain','Test-GuidePdfFonts','New-GuidePdf','Test-GuidePdfCache','Get-GuideWrapperStatus','Get-GuideAssessment','Get-GuideDownloadRequirements','Test-GuideDownloadPublication') CmdletsToExport=@() VariablesToExport=@() AliasesToExport=@() diff --git a/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psm1 b/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psm1 index e61312f..5ab9623 100644 --- a/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psm1 +++ b/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psm1 @@ -37,3 +37,6 @@ $script:CoreRoot=$PSScriptRoot . (Join-Path $PSScriptRoot 'TranslationReadiness/Get-GuideTranslationWork.ps1') . (Join-Path $PSScriptRoot 'TranslationReadiness/Edit-GuideTranslation.ps1') + +. (Join-Path $PSScriptRoot 'TranslationReadiness/Get-GuideSiteTranslationWork.ps1') +. (Join-Path $PSScriptRoot 'ContributorManagement/Add-GuideContribution.ps1') diff --git a/system/OpenGuidePlatform.PowerShell.Core/README.md b/system/OpenGuidePlatform.PowerShell.Core/README.md index fea1318..5c06ed2 100644 --- a/system/OpenGuidePlatform.PowerShell.Core/README.md +++ b/system/OpenGuidePlatform.PowerShell.Core/README.md @@ -14,6 +14,8 @@ PDF language comes from the filename suffix or declared source language and is p `data/contributions/.yml` lists the guide's people with roles `creator` (the authors), `contributor`, `reviewer` or `involved`. `data/contributions/..yml` lists one translation team with roles `translator` or `reviewer`. Each record has `name`, `role` and `contributions` (edition identifiers); optional `weight` orders records and `localizedNames` gives the name to show in a language. `Get-GuideCredits` resolves an edition's authors, contributors and translators; the website and PDF covers use the same records. Prepare blocks invalid roles, unknown editions or files, duplicates and the retired front matter keys `author`, `translators`, `mainfont`, `sansfont`, `monofont` and `dir`, and warns when an edition has no creator or a web translation has no translator. +For a later volunteer joining an existing translation team, `Add-GuideContribution` appends exactly one reviewed record. Supply WorkspaceRoot, Policy, GuideId, Language, ExpectedSha256 and full CandidateYaml. Decode the original UTF-8 bytes without stripping a BOM, then append the new YAML record: the command requires the candidate to preserve the original byte prefix and existing record semantics. It checks translation roles, edition references, localizedNames shape and duplicate identities using the same GitHub-username/name rule as contributor assessment. WhatIf is read-only; protected and stale destinations are refused, including a second hash check before staged replacement. Run Prepare and the site build after applying. This operation does not rename, remove or reorder existing people or add to a guide's own credits. + ### PDF configuration `Get-GuidePdfPlan` builds the recipe from committed configuration only, most specific last: @@ -103,9 +105,11 @@ Set-GuideContent edits only the selected edition's source body. Set-GuideTransla ## Wrapper publishing and readiness -For complete translation creation and source-change reconciliation, follow [Create and reconcile guide translations](TranslationReadiness/README.md). `Get-GuideTranslationWork`, `New-GuideTranslation`, `Test-GuideTranslation` and `Set-GuideTranslation` provide the same workflow to people and agents, including explicit Git source comparisons and reviewed source/target hashes. Existing scaffold and content commands remain available. +For complete site-language creation and later guide-body translation or source-change reconciliation, follow [Add a site language and translate selected guides](TranslationReadiness/README.md). `Get-GuideSiteTranslationWork -WorkspaceRoot ... -Policy ... -Language ...` reports new or existing language configuration, wrapper/catalogue resources, site-owned localized data and guide scaffold work. Add the language disabled in production first, localize the site experience, then create eligible empty guide scaffolds. This is the site-scoped `guide.transcreate` workflow; individual command boundaries do not narrow its scope. Preserve existing translations and protected/PDF-only/fallback intent. Report unsupported operations explicitly rather than inventing direct-write substitutes. + +For the separately selected body stage, `Get-GuideTranslationWork`, `New-GuideTranslation`, `Test-GuideTranslation` and `Set-GuideTranslation` provide the same workflow to people and agents, including explicit Git source comparisons and reviewed source/target hashes. Existing scaffold and content commands remain available. -Set-GuideWrapperTranslation creates or applies exact reviewed candidate text to language-specific wrapper Markdown, YAML catalogues and selected Hugo language configuration entries. Existing files require ExpectedSha256; guide content and supplied-policy protected paths are refused. New languages must be disabled in production, unrelated configuration is preserved, and legacy shared download aliases cannot be extended. Changes are staged per file; a multi-file adoption is not one transaction. +Set-GuideWrapperTranslation creates or applies exact reviewed candidate text to language-specific wrapper Markdown, YAML catalogues and selected Hugo language configuration entries. Existing files require ExpectedSha256; guide edition bodies/resources and supplied-policy protected paths are refused. New languages must be disabled in production, unrelated configuration is preserved, and legacy shared download aliases cannot be extended. Changes are staged per file; a multi-file adoption is not one transaction. Use the installed `./build.ps1 -Stage Prepare` assessment for both human/skill translation status and CI. It supplies effective Hugo catalogue/fallback evidence to Core. Local catalogue diagnostics alone must not replace that assessment. See [shared skill usage](../OpenGuidePlatform.Agents.Integration/skills/USAGE.md). diff --git a/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Get-GuideSiteTranslationWork.ps1 b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Get-GuideSiteTranslationWork.ps1 new file mode 100644 index 0000000..ba20489 --- /dev/null +++ b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Get-GuideSiteTranslationWork.ps1 @@ -0,0 +1,81 @@ +function Get-GuideSiteTranslationWork { + <# .SYNOPSIS + Discover site-language work before selecting individual guide translations. + .DESCRIPTION + Read-only local work inventory, not effective readiness or editorial approval. + JSON text fields require explicit reviewed JSON pointers; no prose is guessed. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$WorkspaceRoot, + [Parameter(Mandatory)][Collections.IDictionary]$Policy, + [Parameter(Mandatory)][ValidatePattern('^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$')][string]$Language + ) + Import-Module powershell-yaml -MinimumVersion 0.4.12 -ErrorAction Stop + $wrapper=$Policy.wrapper.sourcePath.TrimEnd('/') + $configPath="$wrapper/hugo.yaml";$productionPath="$wrapper/hugo.production.yaml" + $config=ConvertFrom-Yaml ([IO.File]::ReadAllText((Resolve-GuideWorkspacePath $WorkspaceRoot $configPath))) + $production=ConvertFrom-Yaml ([IO.File]::ReadAllText((Resolve-GuideWorkspacePath $WorkspaceRoot $productionPath))) + function ConfigValue($name,$fallback){$key=@($config.Keys|Where-Object { $_ -ieq $name });if($key.Count){$config[$key[0]]}else{$fallback}} + $sourceLanguage=[string](ConfigValue 'defaultContentLanguage' 'en') + if($Language -ieq $sourceLanguage){throw 'Select a target language different from the site source language.'} + $disabled=$production.Contains('languages') -and $production.languages.Contains($Language) -and $production.languages[$Language]['disabled'] -eq $true + $declared=$config.Contains('languages') -and $config.languages.Contains($Language) + function FileHash($relative){$file=Resolve-GuideWorkspacePath $WorkspaceRoot $relative;if([IO.File]::Exists($file)){(Get-FileHash -LiteralPath $file).Hash.ToLowerInvariant()}} + function WorkItem($kind,$source,$target,$operation){ + $sourceHash=FileHash $source;$targetHash=FileHash $target + [pscustomobject]@{Kind=$kind;SourcePath=$source;TargetPath=$target;SourceSha256=$sourceHash;TargetSha256=$targetHash;State=if($targetHash){'existing-review-required'}else{'missing'};WriteAllowed=(Test-GuideWritePolicy $Policy $target).Allowed;SupportedOperation=$operation} + } + $contentRelative="$wrapper/$(ConfigValue 'contentDir' 'content')" + $content=Resolve-GuideWorkspacePath $WorkspaceRoot $contentRelative + $wrappers=@( + foreach($extension in @('yaml','yml')){if(Test-Path -LiteralPath (Resolve-GuideWorkspacePath $WorkspaceRoot "$wrapper/i18n/$sourceLanguage.$extension")){WorkItem 'catalogue' "$wrapper/i18n/$sourceLanguage.$extension" "$wrapper/i18n/$Language.$extension" 'Set-GuideWrapperTranslation'}} + if(Test-Path -LiteralPath $content){foreach($file in Get-ChildItem -LiteralPath $content -Filter '*.md' -File -Recurse){ + $relative=[IO.Path]::GetRelativePath($WorkspaceRoot,$file.FullName).Replace('\','/') + $inEdition=$false;foreach($guide in $Policy.guides){foreach($edition in $guide.editions){if($relative.StartsWith("$($guide.contentRoot)/$($edition.path)/",[StringComparison]::OrdinalIgnoreCase)){$inEdition=$true}}} + if($inEdition){continue} + # Hugo default-language source files are unsuffixed or explicitly source-suffixed. + if($file.BaseName -match '\.([A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*)$'){ + if($Matches[1] -ine $sourceLanguage){continue};$stem=$relative.Substring(0,$relative.Length-$sourceLanguage.Length-4) + }else{$stem=$relative.Substring(0,$relative.Length-3)} + WorkItem 'markdown' $relative "$stem.$Language.md" 'Set-GuideWrapperTranslation' + }} + $dataRelative="$wrapper/$(ConfigValue 'dataDir' 'data')" + $data=Resolve-GuideWorkspacePath $WorkspaceRoot $dataRelative + if(Test-Path -LiteralPath $data){foreach($file in Get-ChildItem -LiteralPath $data -Filter "$sourceLanguage.json" -File -Recurse){ + $relative=[IO.Path]::GetRelativePath($WorkspaceRoot,$file.FullName).Replace('\','/') + WorkItem 'json-text-selection-required' $relative ($relative.Substring(0,$relative.Length-$sourceLanguage.Length-5)+"$Language.json") 'Set-GuideWrapperTranslation -JsonTextPaths' + }} + ) + foreach($item in $wrappers){ + if($item.Kind -eq 'catalogue'){ + $existing=@(foreach($ext in @('yaml','yml')){$candidate="$wrapper/i18n/$Language.$ext";if(FileHash $candidate){$candidate}}) + if($existing.Count -eq 1){$item.TargetPath=$existing[0];$item.TargetSha256=FileHash $existing[0];$item.State='existing-review-required'} + } + if(($item.Kind -eq 'markdown' -and -not $item.TargetPath.StartsWith("$wrapper/content/")) -or ($item.Kind -eq 'json-text-selection-required' -and -not $item.TargetPath.StartsWith("$wrapper/data/"))){$item.SupportedOperation=$null;$item.State='unsupported-custom-directory'} + } + foreach($group in @($wrappers|Group-Object TargetPath|Where-Object Count -gt 1)){foreach($item in $group.Group){$item.State='ambiguous-source';$item.SupportedOperation=$null}} + $guides=@(foreach($guide in $Policy.guides){foreach($edition in $guide.editions){ + $target="$($guide.contentRoot)/$($edition.path)/index.$Language.md";$path=Resolve-GuideWorkspacePath $WorkspaceRoot $target + $translation=@($edition.translations|Where-Object language -CEQ $Language) + $intent=if($translation.Count){$translation[0].intent}else{$null} + $state=if([IO.File]::Exists($path)){if([string]::IsNullOrWhiteSpace((Read-GuideDocument $path).Body)){'empty-stub'}else{'populated'}}else{'missing'} + $excluded=@($Policy.publication.permanentExclusions|Where-Object {($_.subject -eq 'guide' -and $_.id -eq $guide.id) -or ($_.subject -eq 'edition' -and $_.id -eq "$($guide.id)/$($edition.id)")}).Count -gt 0 + $allowed=(Test-GuideWritePolicy $Policy $target).Allowed + [pscustomobject]@{GuideId=$guide.id;EditionId=$edition.id;SourceLanguage=$edition.sourceLanguage;SourceLanguageSelected=($Language -ieq $edition.sourceLanguage);SourcePath="$($guide.contentRoot)/$($edition.path)/index.md";SourceSha256=(FileHash "$($guide.contentRoot)/$($edition.path)/index.md");TargetPath=$target;TargetSha256=(FileHash $target);State=$state;Intent=$intent;Excluded=$excluded;WriteAllowed=$allowed;Downloads=@($translation|ForEach-Object {$_.downloads});CanCreateScaffold=($state -eq 'missing' -and $Language -ine $edition.sourceLanguage -and $allowed -and $disabled -and -not $excluded -and $intent -notin @('pdf-only','fallback'))} + }}) + [pscustomobject]@{ + SiteId=$Policy.siteId;Language=$Language;SourceLanguage=$sourceLanguage + Configuration=[pscustomobject]@{MainPath=$configPath;MainSha256=(FileHash $configPath);LanguageDeclared=$declared;ProductionPath=$productionPath;ProductionSha256=(FileHash $productionPath);ProductionExplicitlyDisabled=$disabled} + Wrappers=$wrappers;Guides=$guides + Findings=@( + if(-not $disabled){[pscustomobject]@{Code='PRODUCTION_EXCLUSION_REQUIRED';Severity='blocker';Action='Declare the target language disabled in production before creating files.'}} + if(-not $declared){[pscustomobject]@{Code='SITE_LANGUAGE_CONFIGURATION_REQUIRED';Severity='work';Action='Review and add the selected language to the main site configuration.'}} + foreach($item in $wrappers|Where-Object {-not $_.SupportedOperation}){[pscustomobject]@{Code='WRAPPER_SCOPE_UNSUPPORTED';Severity='blocker';Action="Resolve $($item.State): $($item.TargetPath) before claiming site scaffolding complete."}} + [pscustomobject]@{Code='CUSTOM_SCOPE_REVIEW_REQUIRED';Severity='review';Action='Inspect custom mounts, non-JSON data and template-embedded copy; local discovery is not proof of complete effective coverage.'} + [pscustomobject]@{Code='JSON_TEXT_SELECTION_REQUIRED';Severity='review';Action='Review JSON string pointers explicitly; machine-valued strings are not inferred.'} + [pscustomobject]@{Code='EFFECTIVE_READINESS_REQUIRED';Severity='review';Action='Refresh Prepare and validate preview and production after changes; preserve existing bodies and PDF-only/fallback states and select body translation separately.'} + ) + TranslationQualityAssessed=$false;PublicationVerified=$false + } +} diff --git a/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md index 23a351f..ef29838 100644 --- a/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md +++ b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/README.md @@ -1,8 +1,83 @@ -# Create and reconcile guide translations +# Add a site language and translate selected guides These workflows work in PowerShell without an agent. A person supplies translated Markdown through their editor; an agent may propose the same candidate. Both use the same discovery, checks, hash protections and builds. -## Load and select +## Add a language across the site + +Adding a language is a site-scoped operation: configuration, interface catalogue, localized wrapper and site-owned data come first, then empty eligible guide scaffolds. The `guide.transcreate` skill follows this same procedure. Body translation is a separately selected stage. A request to edit one existing guide body remains scoped to that edition; that command boundary does not reduce a new-language request to one edition. + +Start from the adopted repository root in PowerShell 7.4 or later: + +```powershell +$workspace = $PWD.Path +$platform = ./.OpenGuidePlatform/Resolve-OpenGuidePlatform.ps1 -WorkspaceRoot $workspace -UseInstalled +Import-Module "$platform/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1" -Force +Get-Content "$platform/platform.json" +$output = '.processing/site-language/' + [guid]::NewGuid().ToString('N') +./build.ps1 -Stage Prepare -Target preview -OutputPath $output -PlatformSource Path -PlatformPath $platform +$policy = Import-GuidePolicy -Path "$output/discovered-site.json" +$language = 'kn' # Replace with the requested language. +$siteWork = Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language $language +$siteWork.Configuration | Format-List +$siteWork.Wrappers | Format-Table Kind, SourcePath, TargetPath, State, SupportedOperation -Wrap +$siteWork.Guides | Format-Table GuideId, EditionId, State, Intent, CanCreateScaffold -Wrap +$siteWork.Findings | Format-List +``` + +Read the installed instructions and record the package version and Prepare report used. A missing command requires the coordinated Update workflow when authorized; do not use another Core version or a direct-write substitute. Inspect failed Prepare assessments before continuing: valid discovery may support repairs, but no valid discovery means the operation is blocked. The site report accepts a language not yet configured; it is a work inventory, not translation-quality or publication approval. + +1. Review the report's production configuration first. Prepare an exact candidate with the requested language explicitly `disabled: true`; preserve every unrelated setting. Apply it with `Set-GuideWrapperTranslation` and the reviewed configuration hash, then add the main language entry through the same command. Never enable production during creation. If the site configuration itself is missing, report the setup prerequisite rather than inventing a generic configuration. +2. Refresh Prepare and the site report. Review each wrapper source and target: site homepage, guide roots, history, translations pages and other discovered wrapper Markdown, plus i18n catalogues. Translate reader-facing text while preserving keys, placeholders, URLs, metadata conventions and legacy aliases. Use `Set-GuideWrapperTranslation` for each reviewed candidate. Existing destinations require their original reviewed `ExpectedSha256`; preserve populated translations unless updates were requested. +3. Review site-owned localized data against its actual schema. For supported JSON resources, explicitly select translatable string leaves with `JsonTextPaths` (RFC 6901 pointers such as `/hero/title`); preserve machine values, keys, arrays and unselected values. Do not translate every string indiscriminately. Unsupported resources remain explicit blockers with their paths and required operation, not silent omissions or improvised edits. +4. Refresh discovery again. Review the report's eligible absent guide targets across the site's actual guide/edition inventory, with no fixed guide count. Use the per-edition `New-GuideTranslation` procedure below for each eligible selection, leaving bodies empty. Preserve populated targets, protected content and declared PDF-only/source-fallback intent. Report skipped targets and reasons. A source-only edition does not by itself require translation, and the site workflow must not silently convert explicit intent. +5. Refresh Prepare, run full preview and production builds against the same package, and inspect the rendered site. Report configuration, localized wrapper/data, empty scaffolds, remaining body work, editorial review and verification separately. An intermediate scaffold may have outstanding wrapper or body findings; a failed build is not a readiness pass. Production exclusion remains in place until separately approved promotion. + +For a wrapper file, choose one exact target path from the report. Complete the production-exclusion and main-configuration prerequisites first. This example captures the reviewed hashes, starts from an existing translation when present, and creates a separate candidate for your editor: + +```powershell +$targetPath = Read-Host 'Paste the exact TargetPath from the wrapper report' +$wrapperSelections = @($siteWork.Wrappers | Where-Object TargetPath -CEQ $targetPath) +if ($wrapperSelections.Count -ne 1 -or -not $wrapperSelections[0].SupportedOperation) { + throw 'Select one unambiguous supported wrapper operation.' +} +$wrapper = $wrapperSelections[0] +$reviewedTargetHash = $wrapper.TargetSha256 +$reviewedSourceHash = $wrapper.SourceSha256 +$startingPath = if ($reviewedTargetHash) { $wrapper.TargetPath } else { $wrapper.SourcePath } +$startingFile = Resolve-GuideWorkspacePath $workspace $startingPath +$candidatePath = Join-Path $workspace "$output/wrapper-candidate$([IO.Path]::GetExtension($targetPath))" +[IO.File]::WriteAllText($candidatePath, [IO.File]::ReadAllText($startingFile), [Text.UTF8Encoding]::new($false)) +# Open $candidatePath in your editor. Translate the agreed reader-facing fields and save. +# For JSON, preserve every unselected value and review the string pointers below. +``` + +After editing, apply the exact candidate against those original hashes: + +```powershell +$wrapperChange = @{ + WorkspaceRoot = $workspace; Policy = $policy; Language = $language + RelativePath = $wrapper.TargetPath + CandidateContent = [IO.File]::ReadAllText($candidatePath) +} +if ($reviewedTargetHash) { $wrapperChange.ExpectedSha256 = $reviewedTargetHash } +if ($wrapper.Kind -eq 'json-text-selection-required') { + $wrapperChange.ExpectedSourceSha256 = $reviewedSourceHash + $wrapperChange.JsonTextPaths = @( + (Read-Host 'Enter the reviewed RFC 6901 pointer to one translated string leaf') + ) + # Add further explicitly reviewed pointers if more than one string was translated. +} +Set-GuideWrapperTranslation @wrapperChange -WhatIf +Set-GuideWrapperTranslation @wrapperChange +``` + +A new target omits `ExpectedSha256`; creation refuses an existing destination. JSON pointers such as `/hero/title` select actual string leaves in that site's schema, not a universal field list. Existing JSON starts from the target; new JSON starts from the source so unselected values remain intact. Inspect the source and candidate together and do not refresh a stale hash merely to apply an old candidate. + +Configuration candidates instead start from the existing path and hash in `$siteWork.Configuration` (`ProductionPath`/`ProductionSha256` first, then `MainPath`/`MainSha256`). Edit only the selected language mapping and supply that configuration's hash as `ExpectedSha256` to the same writer. They are not wrapper-source entries and do not use JSON pointers. + +Read command help before applying a candidate and review the actual diff after each operation. Multi-file changes are not one transaction: inspect partial progress and refresh reports before resuming. + +## Select a guide body for a separate translation stage Follow [Core setup](../README.md#load-the-installed-commands-and-discover-content) to resolve `$platform`, import Core, run Prepare into a fresh `$output`, and load `$policy` from its `discovered-site.json`. Use the repository root as `$workspace`. Keep Prepare and subsequent builds on that same package with `-PlatformSource Path -PlatformPath $platform`. @@ -23,7 +98,7 @@ The source language comes from the edition. An absent target's proposed path doe Translation availability is independent for each guide edition. A French translation of v1 does not imply a French translation of v2. Editing v1 requires no v2 translation and never edits or creates one. Explicit creation in v2 creates only that target. Source corrections use Set-GuideContent; all translated-document corrections use Set-GuideTranslation with that edition's source and target hashes. The shared writer rejects a source path from another edition and cannot substitute an existing translation from elsewhere. -## Create a new translation +## Create one eligible empty guide scaffold ```powershell New-GuideTranslation @selection -WhatIf diff --git a/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Set-GuideWrapperTranslation.ps1 b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Set-GuideWrapperTranslation.ps1 index 563892d..22f9d30 100644 --- a/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Set-GuideWrapperTranslation.ps1 +++ b/system/OpenGuidePlatform.PowerShell.Core/TranslationReadiness/Set-GuideWrapperTranslation.ps1 @@ -10,18 +10,31 @@ function Set-GuideWrapperTranslation { [Parameter(Mandatory)][ValidatePattern('^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$')][string]$Language, [Parameter(Mandatory)][string]$RelativePath, [Parameter(Mandatory)][string]$CandidateContent, - [ValidatePattern('^[a-fA-F0-9]{64}$')][string]$ExpectedSha256 + [ValidatePattern('^[a-fA-F0-9]{64}$')][string]$ExpectedSha256, + [string[]]$JsonTextPaths, + [ValidatePattern('^[a-fA-F0-9]{64}$')][string]$ExpectedSourceSha256 ) $target=Resolve-GuideWorkspacePath $WorkspaceRoot $RelativePath + $canonical=[IO.Path]::GetRelativePath([IO.Path]::GetFullPath($WorkspaceRoot),$target).Replace('\','/') + if($canonical -cne $RelativePath){throw 'Use a canonical repository-relative wrapper path without dot segments, repeated separators or alternate separators.'} Assert-GuideWriteAllowed $Policy $RelativePath $wrapper=$Policy.wrapper.sourcePath.TrimEnd('/') foreach($guide in $Policy.guides){ - if($RelativePath.Equals($guide.contentRoot,[StringComparison]::OrdinalIgnoreCase) -or $RelativePath.StartsWith($guide.contentRoot.TrimEnd('/')+'/',[StringComparison]::OrdinalIgnoreCase)){throw 'Wrapper operations must not modify guide content; select the guide operation.'} + if($RelativePath.StartsWith($guide.contentRoot.TrimEnd('/')+'/',[StringComparison]::OrdinalIgnoreCase)){ + foreach($edition in $guide.editions){ + if($RelativePath.StartsWith("$($guide.contentRoot)/$($edition.path)/",[StringComparison]::OrdinalIgnoreCase)){throw 'Wrapper operations must not modify guide content; select the guide operation.'} + } + $siteWork=Get-GuideSiteTranslationWork -WorkspaceRoot $WorkspaceRoot -Policy $Policy -Language $Language + $sourceWrappers=@($siteWork.Wrappers|Where-Object { $_.Kind -eq 'markdown' -and $_.TargetPath -ceq $RelativePath -and $_.SupportedOperation -eq 'Set-GuideWrapperTranslation' }) + if($sourceWrappers.Count -ne 1){throw 'Wrapper operations must not modify undiscovered or ambiguous guide content; select the guide operation.'} + } } $configuration=$RelativePath -cin @("$wrapper/hugo.yaml","$wrapper/hugo.production.yaml") $catalogue=$RelativePath -cin @("$wrapper/i18n/$Language.yaml","$wrapper/i18n/$Language.yml") $markdown=$RelativePath.StartsWith("$wrapper/content/",[StringComparison]::Ordinal) -and $RelativePath.EndsWith(".$Language.md",[StringComparison]::Ordinal) - if(-not ($configuration -or $catalogue -or $markdown)){throw 'Select a language-specific wrapper Markdown/catalogue file or its Hugo language configuration.'} + $json=$RelativePath.StartsWith("$wrapper/data/",[StringComparison]::Ordinal) -and $RelativePath.EndsWith("/$Language.json",[StringComparison]::Ordinal) + if($JsonTextPaths -and -not $json){throw 'JsonTextPaths applies only to discovered language JSON data.'} + if(-not ($configuration -or $catalogue -or $markdown -or $json)){throw 'Select a language-specific wrapper Markdown/catalogue file or its Hugo language configuration.'} $exists=[IO.File]::Exists($target) if($exists -and -not $ExpectedSha256){throw 'Wrapper file exists; preserve it or supply its reviewed ExpectedSha256.'} if($ExpectedSha256 -and (-not $exists -or (Get-FileHash -LiteralPath $target).Hash -ine $ExpectedSha256)){throw 'Wrapper file changed since review or is missing.'} @@ -50,6 +63,34 @@ function Set-GuideWrapperTranslation { $ids=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) foreach($entry in $catalog){if($entry -isnot [Collections.IDictionary] -or -not $entry.Contains('id') -or [string]::IsNullOrWhiteSpace($entry.id) -or -not $ids.Add($entry.id)){throw 'Catalogue entries require unique nonempty ids.'}} } + }elseif($json){ + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $WorkspaceRoot -Policy $Policy -Language $Language + $selected=@($work.Wrappers|Where-Object { $_.Kind -eq 'json-text-selection-required' -and $_.TargetPath -ceq $RelativePath }) + if($selected.Count -ne 1){throw 'Select discovered source-language JSON data; arbitrary JSON paths are unsupported.'} + $sourceFile=Resolve-GuideWorkspacePath $WorkspaceRoot $selected[0].SourcePath + if(-not $ExpectedSourceSha256 -or (Get-FileHash -LiteralPath $sourceFile).Hash -ine $ExpectedSourceSha256){throw 'JSON source changed since review or ExpectedSourceSha256 is missing.'} + if(-not $JsonTextPaths -or @($JsonTextPaths|Sort-Object -Unique).Count -ne $JsonTextPaths.Count){throw 'Select unique reviewed JSON string pointers using JsonTextPaths.'} + $sourceJson=ConvertFrom-Json ([IO.File]::ReadAllText($sourceFile)) -AsHashtable -NoEnumerate + $baseline=if($exists){ConvertFrom-Json ([IO.File]::ReadAllText($target)) -AsHashtable -NoEnumerate}else{$sourceJson} + $candidateJson=ConvertFrom-Json $CandidateContent -AsHashtable -NoEnumerate + $seen=[Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal) + function Compare-JsonText($source,$before,$after,[string]$pointer){ + if($source -is [Collections.IDictionary]){ + if($before -isnot [Collections.IDictionary] -or $after -isnot [Collections.IDictionary] -or $before.Count -ne $source.Count -or $after.Count -ne $source.Count){throw 'Preserve JSON structure and keys.'} + foreach($key in $source.Keys){ + if(-not $before.Contains($key) -or -not $after.Contains($key)){throw 'Preserve JSON structure and keys.'} + Compare-JsonText $source[$key] $before[$key] $after[$key] ($pointer+'/'+$key.Replace('~','~0').Replace('/','~1')) + } + }elseif($source -is [Collections.IList]){ + if($before -isnot [Collections.IList] -or $after -isnot [Collections.IList] -or $before.Count -ne $source.Count -or $after.Count -ne $source.Count){throw 'Preserve JSON array structure.'} + for($i=0;$i -lt $source.Count;$i++){Compare-JsonText $source[$i] $before[$i] $after[$i] "$pointer/$i"} + }elseif($pointer -cin $JsonTextPaths){ + if($source -isnot [string] -or $before -isnot [string] -or $after -isnot [string]){throw 'JSON text pointers must select string leaves.'} + $null=$seen.Add($pointer) + }elseif(-not (Test-GuideValueEqual $before $after)){throw "Preserve unselected JSON value at $pointer."} + } + Compare-JsonText $sourceJson $baseline $candidateJson '' + if($seen.Count -ne $JsonTextPaths.Count){throw 'Every JSON text pointer must select an existing string leaf.'} }else{ $match=[regex]::Match($CandidateContent,'\A---\r?\n(?.*?)\r?\n---(?:\r?\n|\z)(?.*)\z',[Text.RegularExpressions.RegexOptions]::Singleline) if(-not $match.Success){throw 'Expected wrapper Markdown with YAML front matter.'} @@ -77,6 +118,7 @@ function Set-GuideWrapperTranslation { New-GuideFile $temporary $CandidateContent $checked=Resolve-GuideWorkspacePath $WorkspaceRoot $RelativePath Assert-GuideWriteAllowed $Policy $RelativePath + if($json -and (Get-FileHash -LiteralPath $sourceFile).Hash -ine $ExpectedSourceSha256){throw 'JSON source changed during preparation; candidate not applied.'} if($exists){ if(-not [IO.File]::Exists($checked) -or (Get-FileHash -LiteralPath $checked).Hash -ine $ExpectedSha256){throw 'Wrapper file changed during preparation; candidate not applied.'} [IO.File]::Replace($temporary,$checked,[System.Management.Automation.Language.NullString]::Value) @@ -86,4 +128,4 @@ function Set-GuideWrapperTranslation { } [pscustomobject]@{Status=if($exists){'updated'}else{'created'};Path=$RelativePath;Sha256=$digest;Language=$Language;ReadinessRequiresPrepare=$true} } -} \ No newline at end of file +} diff --git a/system/OpenGuidePlatform.PowerShell.PlatformBuild/OpenGuidePlatform.PowerShell.PlatformBuild.psm1 b/system/OpenGuidePlatform.PowerShell.PlatformBuild/OpenGuidePlatform.PowerShell.PlatformBuild.psm1 index 564d2d3..1c96707 100644 --- a/system/OpenGuidePlatform.PowerShell.PlatformBuild/OpenGuidePlatform.PowerShell.PlatformBuild.psm1 +++ b/system/OpenGuidePlatform.PowerShell.PlatformBuild/OpenGuidePlatform.PowerShell.PlatformBuild.psm1 @@ -40,6 +40,8 @@ function Test-PlatformCandidateSample { # sample copy; never edit the source sample or a consumer repository. & (Join-Path $PSHOME $(if($IsWindows){'pwsh.exe'}else{'pwsh'})) -NoProfile -File "$PSScriptRoot/Testing/Test-ContributorWorkflow.ps1" -WorkspaceRoot $WorkspaceRoot -CandidateRoot $candidate -OutputPath $OutputPath if($LASTEXITCODE -ne 0){throw 'Packaged contributor workflow failed.'} + & (Join-Path $PSHOME $(if($IsWindows){'pwsh.exe'}else{'pwsh'})) -NoProfile -File "$PSScriptRoot/Testing/Test-SiteTranslationWorkflow.ps1" -WorkspaceRoot $WorkspaceRoot -CandidateRoot $candidate -OutputPath $OutputPath + if($LASTEXITCODE -ne 0){throw 'Packaged site translation workflow failed.'} } function Invoke-PlatformBuild { [CmdletBinding()] diff --git a/system/OpenGuidePlatform.PowerShell.PlatformBuild/Testing/Test-SiteTranslationWorkflow.ps1 b/system/OpenGuidePlatform.PowerShell.PlatformBuild/Testing/Test-SiteTranslationWorkflow.ps1 new file mode 100644 index 0000000..21e5441 --- /dev/null +++ b/system/OpenGuidePlatform.PowerShell.PlatformBuild/Testing/Test-SiteTranslationWorkflow.ps1 @@ -0,0 +1,148 @@ +#Requires -Version 7.4 +[CmdletBinding()] +param([Parameter(Mandatory)][string]$WorkspaceRoot,[Parameter(Mandatory)][string]$CandidateRoot,[Parameter(Mandatory)][string]$OutputPath) +$ErrorActionPreference='Stop' +Import-Module "$CandidateRoot/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1" -Force +Import-Module powershell-yaml -MinimumVersion 0.4.12 +$instructions=Get-Content "$CandidateRoot/system/OpenGuidePlatform.Agents.Integration/instructions/guide-site.md" -Raw +foreach($required in @('guide.transcreate','guide.transstatus','guide.transreconcile','Get-GuideSiteTranslationWork','separately selected stage','do not enforce permissions')){ + if(-not $instructions.Contains($required)){throw "Packaged contributor instructions omit site translation contract: $required"} +} +$skill=Get-Content "$CandidateRoot/system/OpenGuidePlatform.Agents.Integration/skills/guide.transcreate/SKILL.md" -Raw +if($skill -notmatch 'Get-GuideSiteTranslationWork'){throw 'Packaged translation creation skill does not route through the site work inventory.'} +# The installation adapter distributes these exact bytes to the canonical agent +# file. This checks the package contract, not whether an AI obeys instructions. +$adapter=Get-Content "$CandidateRoot/system/OpenGuidePlatform.PowerShell.GuideSiteAdoption/OpenGuidePlatform.PowerShell.GuideSiteAdoption.psm1" -Raw +if(-not $adapter.Contains("`$files['.agents/agents.md']=`$instructions")){throw 'Installation no longer distributes the reviewed canonical instructions.'} +$scratch="$OutputPath/site-translation-"+[guid]::NewGuid().ToString('N') +$source="$scratch/source" +$destination=Resolve-GuideWorkspacePath $WorkspaceRoot $source +[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination))|Out-Null +Copy-Item -LiteralPath "$WorkspaceRoot/examples/reference-guide-site" -Destination $destination -Recurse +$shell=Join-Path $PSHOME $(if($IsWindows){'pwsh.exe'}else{'pwsh'}) +function Invoke-CandidateBuild([string]$Stage,[string]$Target,[string]$Output) { + & $shell -NoProfile -File "$CandidateRoot/build.ps1" -Product GuideSite -WorkspaceRoot $WorkspaceRoot -SourcePath $source -Stage $Stage -Target $Target -OutputPath $Output + if($LASTEXITCODE -ne 0){throw "Site translation workflow $Stage/$Target failed. See $Output."} +} +function Assert-Refused([scriptblock]$Action,[string]$Message) { + $refused=$false + try{& $Action|Out-Null}catch{$refused=$true} + if(-not $refused){throw $Message} +} +function Read-Relative([string]$Path){[IO.File]::ReadAllText((Resolve-GuideWorkspacePath $WorkspaceRoot $Path))} + +# Synthetic data exists only in this disposable fixture. Explicitly selected prose +# exercises JSON safety without claiming a semantic translation of real content. +$main=Get-Content "$destination/hugo.yaml" -Raw|ConvertFrom-Yaml +$sourceLanguage=[string]$main.defaultContentLanguage +$language=@('de','kn','fr'|Where-Object {-not $main.languages.Contains($_)})|Select-Object -First 1 +if(-not $language){throw 'Site translation acceptance needs a language absent from sample configuration.'} +$jsonDirectory="$destination/data/site-translation-fixture" +[IO.Directory]::CreateDirectory($jsonDirectory)|Out-Null +[IO.File]::WriteAllText("$jsonDirectory/$sourceLanguage.json",'{"title":"Synthetic source title","url":"/preserve-me/","enabled":true,"count":3}') +$before=@{} +foreach($file in Get-ChildItem $destination -File -Recurse){$before[$file.FullName]=(Get-FileHash -LiteralPath $file.FullName).Hash} +Invoke-CandidateBuild Prepare preview "$scratch/initial" +$policy=Import-GuidePolicy "$WorkspaceRoot/$scratch/initial/discovered-site.json" +$siteSelection=@{WorkspaceRoot=$WorkspaceRoot;Policy=$policy;Language=$language} +$initial=Get-GuideSiteTranslationWork @siteSelection +if($initial.Configuration.LanguageDeclared -or $initial.Configuration.ProductionExplicitlyDisabled -or @($initial.Wrappers|Where-Object State -ne 'missing').Count){throw 'Fixture language is not wholly absent.'} +if(@($initial.Guides|Where-Object State -ne 'missing').Count){throw 'Fixture already has a selected-language guide body.'} + +$production=Read-Relative $initial.Configuration.ProductionPath|ConvertFrom-Yaml +$production.languages[$language]=@{disabled=$true} +$productionCandidate=ConvertTo-Yaml $production +$main.languages[$language]=@{label='Synthetic acceptance language';weight=999;title='Synthetic acceptance title'} +$mainCandidate=ConvertTo-Yaml $main +Assert-Refused {Set-GuideWrapperTranslation @siteSelection -RelativePath $initial.Configuration.MainPath -CandidateContent $mainCandidate -ExpectedSha256 $initial.Configuration.MainSha256} 'Main language creation must require production exclusion first.' +Set-GuideWrapperTranslation @siteSelection -RelativePath $initial.Configuration.ProductionPath -CandidateContent $productionCandidate -ExpectedSha256 $initial.Configuration.ProductionSha256|Out-Null +Set-GuideWrapperTranslation @siteSelection -RelativePath $initial.Configuration.MainPath -CandidateContent $mainCandidate -ExpectedSha256 $initial.Configuration.MainSha256|Out-Null +Invoke-CandidateBuild Prepare preview "$scratch/configured" +$siteSelection.Policy=Import-GuidePolicy "$WorkspaceRoot/$scratch/configured/discovered-site.json" + +# Site-first discovery must include guide landing/history/translations wrappers, +# but never classify edition body files as wrapper work. +$configured=Get-GuideSiteTranslationWork @siteSelection +foreach($guide in $policy.guides){ + foreach($suffix in @('/_index.md','/history/index.md','/translations/index.md')){ + $expected=$guide.contentRoot+$suffix + if(Test-Path -LiteralPath (Resolve-GuideWorkspacePath $WorkspaceRoot $expected)){ + if(@($configured.Wrappers|Where-Object SourcePath -CEQ $expected).Count -ne 1){throw "Site work omitted wrapper $expected"} + } + } +} +foreach($wrapper in $configured.Wrappers){ + if($wrapper.SourcePath -cin @($configured.Guides.SourcePath)){throw 'Edition bodies were classified as wrappers.'} + $candidate=Read-Relative $wrapper.SourcePath + $arguments=@{RelativePath=$wrapper.TargetPath;CandidateContent=$candidate} + if($wrapper.Kind -eq 'markdown'){ + # Remove forbidden shared aliases from the new-language candidate only. + $match=[regex]::Match($candidate,'\A---\r?\n(?.*?)\r?\n---(?:\r?\n|\z)(?.*)\z',[Text.RegularExpressions.RegexOptions]::Singleline) + $metadata=ConvertFrom-Yaml $match.Groups['yaml'].Value + if($metadata.Contains('aliases')){ + $aliases=@($metadata.aliases|Where-Object {$_ -notmatch '^/(?:[A-Za-z0-9-]+/)?(?:download|downloads|translationsdirectory)/?$'}) + if($aliases.Count){$metadata.aliases=$aliases}else{$metadata.Remove('aliases')|Out-Null} + } + $arguments.CandidateContent="---`n$((ConvertTo-Yaml $metadata).TrimEnd())`n---`n$($match.Groups['body'].Value)" + }elseif($wrapper.Kind -eq 'json-text-selection-required'){ + $json=$candidate|ConvertFrom-Json -AsHashtable + $json.title='Synthetic target title' + $arguments.CandidateContent=$json|ConvertTo-Json -Depth 20 + $arguments.JsonTextPaths=@('/title') + $arguments.ExpectedSourceSha256=$wrapper.SourceSha256 + } + Set-GuideWrapperTranslation @siteSelection @arguments|Out-Null +} +Invoke-CandidateBuild Prepare preview "$scratch/wrappers" +$siteSelection.Policy=Import-GuidePolicy "$WorkspaceRoot/$scratch/wrappers/discovered-site.json" +$wrapperWork=Get-GuideSiteTranslationWork @siteSelection +$eligible=@($wrapperWork.Guides|Where-Object CanCreateScaffold) +foreach($guide in $eligible){ + New-GuideTranslation @siteSelection -GuideId $guide.GuideId -EditionId $guide.EditionId|Out-Null +} +Invoke-CandidateBuild Prepare preview "$scratch/scaffold" +$siteSelection.Policy=Import-GuidePolicy "$WorkspaceRoot/$scratch/scaffold/discovered-site.json" +$scaffold=Get-GuideSiteTranslationWork @siteSelection +if(@($scaffold.Wrappers|Where-Object State -eq 'missing').Count -or @($scaffold.Guides|Where-Object State -eq 'populated').Count){throw 'Site scaffolding omitted wrappers or populated guide bodies.'} +if(-not @($scaffold.Guides|Where-Object State -eq 'empty-stub').Count){throw 'Site scaffolding did not create any empty edition stub.'} +foreach($expected in $eligible){ + $actual=@($scaffold.Guides|Where-Object TargetPath -CEQ $expected.TargetPath) + if($actual.Count -ne 1 -or $actual[0].State -ne 'empty-stub' -or -not [IO.File]::Exists((Resolve-GuideWorkspacePath $WorkspaceRoot $expected.TargetPath))){throw "Site scaffolding omitted an eligible edition: $($expected.TargetPath)"} +} +foreach($path in $before.Keys){ + if($path -notin @((Resolve-GuideWorkspacePath $WorkspaceRoot $initial.Configuration.MainPath),(Resolve-GuideWorkspacePath $WorkspaceRoot $initial.Configuration.ProductionPath)) -and (Get-FileHash -LiteralPath $path).Hash -cne $before[$path]){throw "Site scaffolding changed original content: $path"} +} +$scaffold|ConvertTo-Json -Depth 30|Set-Content "$WorkspaceRoot/$scratch/site-scaffold-report.json" + +# A separate, explicit edition selection is necessary before any guide body edit. +$selected=$scaffold.Guides|Where-Object State -eq 'empty-stub'|Select-Object -First 1 +$editionSelection=@{WorkspaceRoot=$WorkspaceRoot;Policy=$siteSelection.Policy;Language=$language;GuideId=$selected.GuideId;EditionId=$selected.EditionId} +$work=Get-GuideTranslationWork @editionSelection +$candidate=$work.Target.Content+$work.Source.Body+"`n`nSynthetic translation acceptance fixture; no language-quality claim.`n" +Set-GuideTranslation @editionSelection -CandidateContent $candidate -ExpectedSha256 $work.Target.Sha256 -ExpectedSourceSha256 $work.Source.Sha256|Out-Null +$populatedHash=(Get-FileHash (Resolve-GuideWorkspacePath $WorkspaceRoot $selected.TargetPath)).Hash +if((New-GuideTranslation @editionSelection).Status -ne 'preserved' -or (Get-FileHash (Resolve-GuideWorkspacePath $WorkspaceRoot $selected.TargetPath)).Hash -cne $populatedHash){throw 'Scaffold rerun did not preserve the populated target.'} +Assert-Refused {Set-GuideWrapperTranslation @siteSelection -RelativePath $selected.TargetPath -CandidateContent $candidate -ExpectedSha256 $populatedHash} 'Wrapper operation accepted an edition body.' +$wrapper=$scaffold.Wrappers|Where-Object Kind -eq 'markdown'|Select-Object -First 1 +$wrapperCandidate=(Read-Relative $wrapper.TargetPath)+"`nSynthetic wrapper acceptance update.`n" +Set-GuideWrapperTranslation @siteSelection -RelativePath $wrapper.TargetPath -CandidateContent $wrapperCandidate -ExpectedSha256 $wrapper.TargetSha256|Out-Null +Assert-Refused {Set-GuideWrapperTranslation @siteSelection -RelativePath $wrapper.TargetPath -CandidateContent $wrapperCandidate -ExpectedSha256 $wrapper.TargetSha256} 'Wrapper update accepted a stale hash.' +$final=Get-GuideSiteTranslationWork @siteSelection +if(@($final.Guides|Where-Object State -eq 'populated').Count -ne 1){throw 'Selected edition translation changed unrelated edition bodies.'} +foreach($other in $scaffold.Guides|Where-Object {$_.TargetPath -cne $selected.TargetPath -and $_.TargetSha256}){ + if((Get-FileHash (Resolve-GuideWorkspacePath $WorkspaceRoot $other.TargetPath)).Hash -ine $other.TargetSha256){throw "Selected translation changed another edition stub: $($other.TargetPath)"} +} +foreach($target in @('preview','production')){Invoke-CandidateBuild All $target "$scratch/$target"} +$configurationPaths=@((Resolve-GuideWorkspacePath $WorkspaceRoot $initial.Configuration.MainPath),(Resolve-GuideWorkspacePath $WorkspaceRoot $initial.Configuration.ProductionPath)) +foreach($path in $before.Keys){ + if($path -notin $configurationPaths -and (Get-FileHash -LiteralPath $path).Hash -cne $before[$path]){throw "Site translation changed existing content or supplied PDF: $path"} +} +if(-not (Test-Path "$WorkspaceRoot/$scratch/preview/site/$language/index.html")){throw 'Preview omitted the new site language route.'} +if(Test-Path "$WorkspaceRoot/$scratch/production/site/$language"){throw 'Production published the excluded site language.'} +[ordered]@{ + schemaVersion=1;outcome='pass';packageRoot=$CandidateRoot;language=$language + selectedGuide=$selected.GuideId;selectedEdition=$selected.EditionId;translationQualityAssessed=$false + wrapperCount=@($final.Wrappers).Count;editionCount=@($final.Guides).Count + verification=@('absent site language','production exclusion before main configuration','guide subpath wrappers discovered','explicit JSON text selection','site scaffold report before body translation','empty edition stubs','one selected body','rerun preservation','stale wrapper hash refused','edition body refused by wrapper writer','existing content and PDFs preserved','preview route present','production route absent') +}|ConvertTo-Json -Depth 5|Set-Content "$WorkspaceRoot/$scratch/result.json" +Write-Host "PASS packaged site translation workflow. Evidence: $scratch/result.json" diff --git a/tests/Core/GuideCredits.Tests.ps1 b/tests/Core/GuideCredits.Tests.ps1 index c01d50a..67d7e08 100644 --- a/tests/Core/GuideCredits.Tests.ps1 +++ b/tests/Core/GuideCredits.Tests.ps1 @@ -68,6 +68,34 @@ Describe 'Contributor records and credits' { It 'reports no findings for valid data' { @(Find-ContributorIssues $workspace $policy|Where-Object Severity -eq blocker).Count | Should -Be 0 } + It 'appends one reviewer while preserving comments, BOM, line endings and every original byte' { + $path=Join-Path $workspace "$data/example.fa.yml" + $original=[char]0xFEFF+"# Keep this comment`r`n"+[IO.File]::ReadAllText($path).Replace("`n","`r`n").TrimEnd() + [IO.File]::WriteAllText($path,$original,[Text.UTF8Encoding]::new($false)) + $hash=(Get-FileHash $path).Hash + $candidate=$original+"`r`n- name: New Reviewer`r`n role: reviewer`r`n contributions: [`"$editionId`"]`r`n" + Add-GuideContribution $workspace $policy example fa $hash $candidate -WhatIf + (Get-FileHash $path).Hash | Should -Be $hash + $result=Add-GuideContribution $workspace $policy example fa $hash $candidate + $result.Status | Should -Be appended + [IO.File]::ReadAllBytes($path) | Should -Be ([Text.UTF8Encoding]::new($false).GetBytes($candidate)) + @(Find-ContributorIssues $workspace $policy|Where-Object Severity -eq blocker).Count | Should -Be 0 + } + It 'rejects unsafe contributor appends without changing the file' { + $path=Join-Path $workspace "$data/example.fa.yml" + $original=[IO.File]::ReadAllText($path);$hash=(Get-FileHash $path).Hash + $suffix="- name: New Reviewer`n role: reviewer`n contributions: [`"$editionId`"]`n" + {Add-GuideContribution $workspace $policy example fa ('0'*64) ($original+$suffix)} | Should -Throw '*changed since review*' + {Add-GuideContribution $workspace $policy example fa $hash ($original.Replace('Parisa','Changed')+$suffix)} | Should -Throw '*existing byte*' + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix+$suffix)} | Should -Throw '*exactly one*' + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix.Replace('New Reviewer','Parisa Translator'))} | Should -Throw '*identity already exists*' + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix.Replace('role: reviewer','role: creator'))} | Should -Throw '*allowed translation role*' + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix.Replace($editionId,'2099.9'))} | Should -Throw '*existing guide editions*' + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix+" localizedNames: wrong`n")} | Should -Throw '*localizedNames*' + $policy.protectedPaths=@("$data/example.fa.yml") + {Add-GuideContribution $workspace $policy example fa $hash ($original+$suffix)} | Should -Throw + (Get-FileHash $path).Hash | Should -Be $hash + } It 'reports invalid roles, unknown editions, duplicates and unknown files' { Write-Fixture "$data/example.es-ES.yml" "- name: Ana`n role: Translator`n contributions: [`"$editionId`"]`n- name: Ana`n role: translator`n contributions: [`"2030.1`"]`n" Write-Fixture "$data/unknown-guide.yml" "- name: Nobody`n role: creator`n contributions: [`"$editionId`"]`n" diff --git a/tests/Core/SiteTranslationWork.Tests.ps1 b/tests/Core/SiteTranslationWork.Tests.ps1 new file mode 100644 index 0000000..401e4db --- /dev/null +++ b/tests/Core/SiteTranslationWork.Tests.ps1 @@ -0,0 +1,95 @@ +BeforeAll { + $root=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent + Import-Module "$root/system/OpenGuidePlatform.PowerShell.Core/OpenGuidePlatform.PowerShell.Core.psd1" -Force +} +Describe 'Site language work inventory' { + BeforeEach { + $workspace=Join-Path $TestDrive ([guid]::NewGuid().ToString('N')) + foreach($folder in @('site/content/a/v1','site/content/b/v2','site/content/a/history','site/i18n','site/data/home')){New-Item -ItemType Directory "$workspace/$folder" -Force|Out-Null} + Set-Content "$workspace/site/hugo.yaml" "defaultContentLanguage: en`nlanguages:`n en:`n languageName: English" + Set-Content "$workspace/site/hugo.production.yaml" "languages:`n en:`n disabled: false" + Set-Content "$workspace/site/i18n/en.yaml" 'home: Home' + Set-Content "$workspace/site/data/home/en.json" '{"title":"Hello","color":"blue","items":["Text"]}' + foreach($file in @('_index.md','a/_index.md','a/history/index.md','a/v1/index.md','b/v2/index.md')){Set-Content "$workspace/site/content/$file" "---`ntitle: Source`n---`nSource body"} + $policy=@{siteId='test';wrapper=@{sourcePath='site';requiredFiles=@('site/content/a/_index.md','site/content/a/history/index.md')};guides=@( + @{id='a';contentRoot='site/content/a';protectSource=$false;editions=@(@{id='v1';path='v1';sourceLanguage='en';translations=@()})}, + @{id='b';contentRoot='site/content/b';protectSource=$false;editions=@(@{id='v2';path='v2';sourceLanguage='en';translations=@()})} + );protectedPaths=@();publication=@{permanentExclusions=@();environments=@()}} + } + It 'discovers an absent language across the site and every guide without writes' { + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn + $work.Configuration.LanguageDeclared|Should -BeFalse + $work.Configuration.ProductionExplicitlyDisabled|Should -BeFalse + $work.Guides.Count|Should -Be 2 + $work.Wrappers.Count|Should -Be 5 + $work.Wrappers.TargetPath|Should -Contain 'site/content/a/history/index.kn.md' + $work.Wrappers.TargetPath|Should -Contain 'site/data/home/kn.json' + @($work.Guides|Where-Object CanCreateScaffold).Count|Should -Be 0 + Test-Path "$workspace/site/content/_index.kn.md"|Should -BeFalse + } + It 'preserves populated, empty and PDF-only states' { + Set-Content "$workspace/site/hugo.production.yaml" "languages:`n kn:`n disabled: true" + Set-Content "$workspace/site/content/a/v1/index.kn.md" "---`ntitle: Kannada`n---`nBody" + $policy.guides[1].editions[0].translations=@(@{language='kn';intent='pdf-only';downloads=@(@{path='supplied.kn.pdf';handling='supplied'})}) + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn + $work.Guides[0].State|Should -Be populated + $work.Guides[1].Intent|Should -Be pdf-only + @($work.Guides|Where-Object CanCreateScaffold).Count|Should -Be 0 + Set-Content "$workspace/site/content/a/v1/index.kn.md" "---`ntitle: Kannada`n---`n" + (Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn).Guides[0].State|Should -Be empty-stub + } + It 'allows discovered guide wrappers but never edition resources or unknown guide files' { + Set-Content "$workspace/site/hugo.production.yaml" "languages:`n kn:`n disabled: true" + $wrapperArgs=@{WorkspaceRoot=$workspace;Policy=$policy;Language='kn';CandidateContent="---`ntitle: Kannada`n---`n"} + (Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/history/index.kn.md).Status|Should -Be created + New-Item -ItemType Directory "$workspace/site/content/a/details"|Out-Null + Set-Content "$workspace/site/content/a/details/index.md" "---`ntitle: Details`n---`nDetails" + (Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/details/index.kn.md).Status|Should -Be created + {Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/v1/index.kn.md}|Should -Throw '*guide content*' + {Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/./a/v1/index.kn.md}|Should -Throw '*canonical*' + {Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content//a/v1/index.kn.md}|Should -Throw '*canonical*' + New-Item -ItemType Directory "$workspace/site/content/a/suffixed"|Out-Null + Set-Content "$workspace/site/content/a/suffixed/index.en.md" "---`ntitle: Details`n---`nDetails" + (Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/suffixed/index.kn.md).Status|Should -Be created + {Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/unknown.kn.md}|Should -Throw '*undiscovered*' + $policy.protectedPaths=@('site/content/a/history') + {Set-GuideWrapperTranslation @wrapperArgs -RelativePath site/content/a/history/index.kn.md}|Should -Throw '*PROTECTED_RESOURCE*' + } + It 'applies only explicitly reviewed JSON text leaves and preserves machine values' { + Set-Content "$workspace/site/hugo.production.yaml" "languages:`n kn:`n disabled: true" + $wrapperArgs=@{WorkspaceRoot=$workspace;Policy=$policy;Language='kn';RelativePath='site/data/home/kn.json';ExpectedSourceSha256=(Get-FileHash "$workspace/site/data/home/en.json").Hash;JsonTextPaths=@('/title','/items/0')} + $candidate='{"title":"Namaskara","color":"blue","items":["Translated"]}' + (Set-GuideWrapperTranslation @wrapperArgs -CandidateContent $candidate).Status|Should -Be created + $wrapperArgs.ExpectedSha256=(Get-FileHash "$workspace/site/data/home/kn.json").Hash + {Set-GuideWrapperTranslation @wrapperArgs -CandidateContent ($candidate.Replace('blue','red'))}|Should -Throw '*unselected*' + {Set-GuideWrapperTranslation @wrapperArgs -CandidateContent '{"title":"X"}'}|Should -Throw '*structure*' + $wrapperArgs.JsonTextPaths=@('/missing') + {Set-GuideWrapperTranslation @wrapperArgs -CandidateContent $candidate}|Should -Throw '*existing string leaf*' + $wrapperArgs.JsonTextPaths=@('/title');$wrapperArgs.ExpectedSourceSha256='0'*64 + {Set-GuideWrapperTranslation @wrapperArgs -CandidateContent $candidate}|Should -Throw '*source changed*' + } + It 'reports source ambiguity and preserves an existing catalogue extension' { + Set-Content "$workspace/site/content/_index.en.md" "---`ntitle: Other`n---`n" + Set-Content "$workspace/site/i18n/kn.yml" 'home: Translated' + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn + @($work.Wrappers|Where-Object State -EQ ambiguous-source).Count|Should -Be 2 + $work.Wrappers.TargetPath|Should -Contain 'site/i18n/kn.yml' + $work.Findings.Code|Should -Contain WRAPPER_SCOPE_UNSUPPORTED + } + It 'reads Hugo configuration casing and reports unsupported custom content directories' { + Set-Content "$workspace/site/hugo.yaml" "defaultcontentlanguage: en`ncontentdir: pages`nlanguages:`n en:`n languageName: English" + New-Item -ItemType Directory "$workspace/site/pages"|Out-Null + Set-Content "$workspace/site/pages/_index.md" "---`ntitle: Home`n---`n" + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn + ($work.Wrappers|Where-Object TargetPath -EQ 'site/pages/_index.kn.md').State|Should -Be unsupported-custom-directory + } + It 'never proposes translating an edition into its own source language' { + Set-Content "$workspace/site/hugo.production.yaml" "languages:`n kn:`n disabled: true" + $policy.guides[1].editions[0].sourceLanguage='kn' + $work=Get-GuideSiteTranslationWork -WorkspaceRoot $workspace -Policy $policy -Language kn + $work.Guides[0].CanCreateScaffold|Should -BeTrue + $work.Guides[1].SourceLanguage|Should -Be kn + $work.Guides[1].SourceLanguageSelected|Should -BeTrue + $work.Guides[1].CanCreateScaffold|Should -BeFalse + } +} \ No newline at end of file