diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml index 2ca5881ea..5f9f8edfd 100644 --- a/.github/workflows/translate.yml +++ b/.github/workflows/translate.yml @@ -7,24 +7,21 @@ name: Translate content (Translated) # # Jobs: # dry_run pull requests only; fully offline (--self-test, --plan, --dump-orders). -# translate push to main (only while the TRANSLATED_ENV variable is -# 'production') and workflow_dispatch (any environment). Folds the open -# translation branch into main's tree, runs --run (synchronous: translate, -# verify, write in one pass), pushes to `localization` (production) or -# `localization-sandbox` (draft PR), and opens/updates the review PR. -# The sandbox account has no AI-enabled service type, so a sandbox -# --run fails at the first submission. +# translate nightly schedule (only while the TRANSLATED_ENV variable is 'production') +# and workflow_dispatch (any environment). One run per night collects every +# English change of the day, so text edited several times is paid for once. +# Folds the open translation branch into main's tree (status files merged +# entry by entry), runs --run (synchronous: translate, verify, write in one +# pass), pushes to `localization` (production) or `localization-sandbox` +# (draft PR), and opens/updates the review PR. Translations, rejected +# deliveries and the report are also uploaded as an artifact, so billed +# work survives a failed push. The sandbox account has no AI-enabled +# service type, so a sandbox --run fails at the first submission. on: - push: - branches: [main] - paths: - - 'content/**' - - 'metadata/build-config.json' - - 'metadata/language-metadata.json' - - 'build_scripts/translate-content.py' - - 'build_scripts/config_loader.py' - - '.github/workflows/translate.yml' + schedule: + # 03:00 UTC daily; scheduled runs use the workflow file on the default branch. + - cron: '0 3 * * *' workflow_dispatch: inputs: mode: @@ -52,6 +49,7 @@ on: paths: - '.github/workflows/translate.yml' - 'build_scripts/translate-content.py' + - 'build_scripts/translation_segments.py' - 'build_scripts/config_loader.py' - 'metadata/build-config.json' - 'metadata/language-metadata.json' @@ -77,6 +75,8 @@ jobs: with: # The PR's own commit, not the synthetic merge commit: test what will be merged. ref: ${{ github.event.pull_request.head.sha }} + # Full history: the plan reads the English versions the translations were made from. + fetch-depth: 0 - uses: actions/setup-python@v5 with: @@ -118,8 +118,8 @@ jobs: translate: name: Submit and collect translations - # Automatic triggers only in production; a dispatch runs in whatever TRANSLATED_ENV names. - if: github.event_name == 'workflow_dispatch' || (github.event_name != 'pull_request' && vars.TRANSLATED_ENV == 'production') + # Scheduled runs only in production; a dispatch runs in whatever TRANSLATED_ENV names. + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && vars.TRANSLATED_ENV == 'production') runs-on: ubuntu-latest timeout-minutes: 45 permissions: @@ -133,6 +133,7 @@ jobs: env: TRANSLATED_ENV: ${{ vars.TRANSLATED_ENV }} TRANSLATED_SERVICE_TYPE: ${{ vars.TRANSLATED_SERVICE_TYPE }} + TRANSLATED_CONTEXT_SEGMENTS: ${{ vars.TRANSLATED_CONTEXT_SEGMENTS }} HAS_KEY: ${{ secrets.TRANSLATED_API_KEY != '' }} steps: - name: Check secret and environment @@ -176,7 +177,7 @@ jobs: INPUT_TARGET_BRANCH: ${{ inputs.target_branch }} INPUT_MODE: ${{ inputs.mode }} run: | - # Empty (the default, and always on push) means auto by environment. + # Empty (the default, and always on a scheduled run) means auto by environment. target="$INPUT_TARGET_BRANCH" if [ -z "$target" ]; then if [ "$TRANSLATED_ENV" = "production" ]; then @@ -246,7 +247,20 @@ jobs: exit 0 fi + # -X theirs resolves conflicting hunks of the status JSON wholesale from the branch, so + # older entries the branch merely carries could overwrite newer ones from main. + # --merge-status rebuilds the status files entry by entry from the three versions. + main_sha="$(git rev-parse HEAD)" + merge_status() { + python build_scripts/translate-content.py --merge-status "$main_sha" "origin/$TARGET" + if [ -n "$(git status --porcelain localizedContent)" ]; then + git add -A localizedContent + git commit -q -m "Merge translation status entry by entry" + fi + } + if git merge --no-edit -X theirs "origin/$TARGET"; then + merge_status exit 0 fi if ! test -f .git/MERGE_HEAD; then @@ -259,6 +273,7 @@ jobs: if git checkout "origin/$TARGET" -- localizedContent \ && git add -A localizedContent \ && { git -c core.editor=true merge --continue || git commit -m "Merge $TARGET (translations)"; }; then + merge_status exit 0 fi unmerged="$(git diff --name-only --diff-filter=U | tr '\n' ' ')" @@ -266,6 +281,11 @@ jobs: git merge --abort || true exit 1 + - name: Record the starting commit + id: start + if: steps.preflight.outputs.ready == 'true' + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Self-test (offline) if: steps.preflight.outputs.ready == 'true' run: python build_scripts/translate-content.py --self-test @@ -273,6 +293,9 @@ jobs: - name: Translate changed content id: translate if: steps.preflight.outputs.ready == 'true' + # Below the job's 45 minutes: a step timeout fails the step, so the push below still + # runs, where a job timeout would cancel the job and discard the billed translations. + timeout-minutes: 35 env: TRANSLATED_API_KEY: ${{ secrets.TRANSLATED_API_KEY }} INPUT_MODE: ${{ inputs.mode }} @@ -282,7 +305,7 @@ jobs: run: | mode="${INPUT_MODE:-translate}" report="$RUNNER_TEMP/translation-report.md" - run_args=(--run --limit "${INPUT_LIMIT:-0}" --report "$report") + run_args=(--run --limit "${INPUT_LIMIT:-0}" --report "$report" --keep-rejected "$RUNNER_TEMP/rejected") if [ "$INPUT_FORCE" = "true" ]; then run_args+=(--force) fi @@ -305,10 +328,26 @@ jobs: ;; esac + - name: Save the translations as a patch + # Billed translations exist only on this runner until the push succeeds. The patch + # (git apply) is uploaded with the report, so a rejected push or a cancelled run + # loses nothing that was paid for. + if: ${{ always() && steps.start.outputs.sha != '' && steps.translate.outcome != 'skipped' }} + env: + START: ${{ steps.start.outputs.sha }} + run: | + git add -A localizedContent + git diff --cached --binary "$START" -- localizedContent > "$RUNNER_TEMP/translations.patch" + if [ -s "$RUNNER_TEMP/translations.patch" ]; then + echo "Saved $(git diff --cached --name-only "$START" -- localizedContent | wc -l) changed file(s) to translations.patch." + else + rm "$RUNNER_TEMP/translations.patch" + fi + - name: Push to the translation branch id: push - # Runs even when the translate step failed, so billed translations are kept. - if: ${{ !cancelled() && steps.translate.outcome != 'skipped' && inputs.mode != 'dry-run' && inputs.mode != 'probe' }} + # Runs even when the translate step failed or the run was cancelled, so billed translations are kept. + if: ${{ always() && steps.translate.outcome != 'skipped' && inputs.mode != 'dry-run' && inputs.mode != 'probe' }} env: TARGET: ${{ steps.target.outputs.branch }} run: | @@ -341,8 +380,8 @@ jobs: - name: Open or update the review PR # No implicit success(): pushed translations get a PR (with a warning line) - # even when the translate step failed. - if: ${{ !cancelled() && steps.push.outputs.pushed == 'true' }} + # even when the translate step failed or the run was cancelled. + if: ${{ always() && steps.push.outputs.pushed == 'true' }} env: GH_TOKEN: ${{ github.token }} TARGET: ${{ steps.target.outputs.branch }} @@ -397,6 +436,8 @@ jobs: path: | ${{ runner.temp }}/translation-report.md ${{ runner.temp }}/orders + ${{ runner.temp }}/translations.patch + ${{ runner.temp }}/rejected - name: Step summary if: always() diff --git a/README.md b/README.md index c0f614440..2da9fea9b 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Run the same checks locally before pushing: `./run scripts check`, `./run doctes # Translating Content -Translations are produced by [Translated](https://translated.com)'s AI translation service through the TranslationOS API (`POST /translate/ai`, service type `AI`) and arrive as review PRs from the `localization` branch, driven by `.github/workflows/translate.yml` and `build_scripts/translate-content.py`. The endpoint is synchronous: each request returns the translation in its response (billed per character), so a run translates, verifies and writes every file in one pass - nothing is pending between runs and there is nothing to poll or cancel. English in `content/` is the only authored source. Hand edits to `localizedContent/{lang}/content/` - on the translation branch or on `main` - are fine, but they are overwritten the next time the English source changes unless the file is pinned (see [Runbook](#runbook)). +Translations are produced by [Translated](https://translated.com)'s AI translation service through the TranslationOS API (`POST /translate/ai`, service type `AI`) and arrive as review PRs from the `localization` branch, driven by `.github/workflows/translate.yml` and `build_scripts/translate-content.py`. The endpoint is synchronous: each request returns the translation in its response (billed per character), so a run translates, verifies and writes every file in one pass - nothing is pending between runs and there is nothing to poll or cancel. English in `content/` is the only authored source. When the English of a translated file changes, only the changed blocks are translated again (see [Segment-level updates](#segment-level-updates)). Hand edits to `localizedContent/{lang}/content/` - on the translation branch or on `main` - are kept, except in blocks whose English changes and in files that are translated as a whole again; pin a file to keep it entirely (see [Runbook](#runbook)). ## Scope and configuration @@ -186,6 +186,7 @@ The `translation` section of `metadata/build-config.json` defines what is transl - **`sources`** - globs relative to `content/`: every `**/*.md` (including the sidebar `toc.md` files), `404.html`, `getting-started/app/**/*.html`, `toc.yml` and `_ui-strings.json`. Files under the shared directories (`assets`, `api`) are always skipped. - **`passthrough`** - `whats-new/**/*.html` and `security/third-party-notices.md`. These files are not translated: every run copies them from English byte-for-byte when their hash differs (or the target is missing) and records them in the status file with status `copied`. A passthrough pattern takes precedence over `sources`, so a file matched by both is copied, never translated. - **`environments`** - the two endpoints the script can talk to: `sandbox` is `https://api-mt.sandbox.translated.com/v2/` and `production` is `https://api-mt.translated.com/v2/` (AI translation lives on the dedicated `api-mt.*` hosts, not the classic `api.*` hosts), both with service type `AI`. `TRANSLATED_ENV` selects which one is active (see below). Note: the sandbox account has no AI-enabled service type (its only type, `economy`, is human-only), so sandbox runs fail at the first submission - production is validated locally with `--limit`/`--file` instead. +- **`segmentDiff`** - `enabled` (default `true`) turns on [segment-level updates](#segment-level-updates); `maxChangedRatio` (default `0.8`) sends a file whole when its changed text exceeds that share of its translatable text; `contextSegments` (default `2`) sends unchanged English blocks with each segment-level update as untranslatable context: `n` sends up to `n` translatable blocks on each side of a changed block, `-1` every unchanged translatable block of the page, `0` none. Translated does not bill context, so the billed characters are the changed units only. The `TRANSLATED_CONTEXT_SEGMENTS` variable overrides it. - **`sandboxLimits`** (`maxFilesPerRun`: 20, `maxCharsPerRun`: 200000; see [Sandbox caps](#sandbox-caps)). - **`instructions`** - file-level notes attached to every order as context for the translation. Glossaries and do-not-translate lists are configured by Translated on the service type itself, not per request. @@ -197,9 +198,10 @@ Every target language folder needs a `translatedLocale` (`es-ES`, `zh-CN`) in `m |----------|----------------|---------| | `TRANSLATED_API_KEY` | repository secret / your shell | API key for the active environment. | | `TRANSLATED_ENV` | repository variable / your shell | `sandbox` or `production`; selects the entry of `translation.environments` to use. There is no default: every network action (`--run`, `--probe`) refuses to run while it is unset or unknown. | +| `TRANSLATED_CONTEXT_SEGMENTS` | repository variable / your shell | Overrides `translation.segmentDiff.contextSegments` (unchanged blocks sent as context on each side of a change; `-1` = whole page, `0` = none). A value that is not an integer stops the run. | | `TRANSLATED_SERVICE_TYPE` | repository variable / your shell | Overrides the environment's `serviceType` (both environments default to `AI`); run `--probe` to list the account's service types. | -Offline actions (`--plan`, `--self-test`, `--dump-orders`, `--baseline`) need neither variable nor key. Before any network call the script prints the resolved environment, base URL and service type - read that line before you continue. +Offline actions (`--plan`, `--self-test`, `--dump-orders`, `--baseline`, `--migrate-blobs`) need neither variable nor key. Before any network call the script prints the resolved environment, base URL and service type - read that line before you continue. ## Command-line reference @@ -207,10 +209,12 @@ Offline actions (`--plan`, `--self-test`, `--dump-orders`, `--baseline`) need ne | Action | What it does | Network | |--------|--------------|---------| -| `--plan` | Print what would happen per language: missing/outdated files to submit, recorded failures, pinned and unscoped entries, orphans, passthrough copies, character counts. | no | +| `--plan` | Print what would happen per language: missing/outdated files to submit (outdated files updated from changed segments show `changed` with their changed/total units and characters; files sent whole instead show the reason), recorded failures, pinned and unscoped entries, orphans, passthrough copies, character counts. | no | | `--run` | Translate missing/outdated files (one synchronous request per file and language), repair and verify each response against its English source, write the translations, copy passthrough files, delete orphaned translations - what CI runs. | yes | | `--baseline` | Seed the status file from the existing translations: accept those that verify (after repair) and mark only the unrepairable ones untranslated (see [Baseline](#baseline)). | no | -| `--self-test` | Offline test suite: identity round-trip over the whole corpus, verifier/repair fixtures, and a fake-client end-to-end `--run` in a temporary mini repo. Exits 1 on any failure; the `pull_request` dry-run job runs it on PRs that touch the translation script or its config, and every `translate` run starts with it. | no | +| `--migrate-blobs` | Record `sourceBlob` for status entries that have only a `sourceHash`: the git blob of the English version whose content hashes to `sourceHash` (current file first, then history). Passthrough files are skipped. Prints per language how many were recorded, already valid and not found. Run it once after a `--baseline`; a run also finds the version on the fly when `sourceBlob` is missing. | no | +| `--merge-status OURS THEIRS` | Rebuild each status file after the translation branch `THEIRS` was merged into `OURS`: an entry the branch added, changed or removed since the merge base comes from the branch, every other entry from `OURS`. The workflow runs it after folding the translation branch in, because the text merge (`-X theirs`) resolves conflicting hunks of the JSON from the branch, so entries the branch only carries in an older state would overwrite newer ones from `main`. | no | +| `--self-test` | Offline test suite: identity round-trip over the whole corpus, segmentation round-trip over every Markdown source, verifier/repair and segmentation fixtures, and fake-client end-to-end `--run`s in temporary mini repos (segment-level updates run in a temporary git repository). Exits 1 on any failure; the `pull_request` dry-run job runs it on PRs that touch the translation script or its config, and every `translate` run starts with it. | no | | `--probe` | Fetch the account's service-type names and language list, print them, and check `sourceLocale`, every `translatedLocale` and the resolved service type against them (exit 1 with the closest matches on a mismatch). | yes | | `--dump-orders DIR` | Build the exact `/translate/ai` request bodies through the same code path as `--run` and write them to `DIR/{lang}/{rel}.json` plus `DIR/summary.json` (counts, characters, environment). Nothing is sent and the status file is not modified. | no | @@ -226,26 +230,52 @@ Options: | `--lenient` | Write deliveries even when verification finds problems - except empty or otherwise unusable text, which is never written, and `toc.yml`/`_ui-strings.json` deliveries, which are always rejected on problems. For local inspection only; the workflow never passes it. | | `--allow-unbounded` | Lift the sandbox caps. The workflow never passes it. | | `--report PATH` | Write the Markdown run report (used as the PR body). | +| `--keep-rejected DIR` | With `--run`: save every billed delivery that fails verification to `DIR/{lang}/{rel}.json` (the order, the response and the problems), so it can be repaired by hand instead of bought again. The workflow passes it and uploads the directory. | | `--baseline-ref REF` | With `--baseline`: hash the English sources at this git ref (via `git show`) instead of the working tree. The ref must resolve to a commit, otherwise the run exits 1. | | `--overwrite-baseline` | With `--baseline`: allow overwriting a status file that already has `files` entries. | | `--repair-existing` | With `--baseline`: also write the repaired text of existing translations that the repair step had to fix (fences, inline code, markers, link targets, HTML attributes restored from English). Without it only the status file changes. | ## How a run works -1. **Plan.** Each English file's SHA-256 (line endings normalized) is compared with the entry in `localizedContent/{lang}/.translation-status.json`. Missing and outdated files are submitted. Files with `"manual": true` are skipped and reported as *pinned*. Files whose recorded failure is at the current source hash are skipped until `--retry-failed` or `--force`. Status entries that are neither in `sources`, `passthrough` nor orphans are reported as *unscoped* and dropped. Translations whose English source no longer exists (*orphans*) are deleted - only when no `--file`/`--limit` filter is active. -2. **Translate.** One synchronous `POST /translate/ai` request per file and language: the whole file goes as a single `translatable` segment (with `target_language`, `content_type` and `service_type`) and the response carries the translation and the billed character count. Every request has an `x-idempotency-id`, reused on retries after a timeout or 5xx, so the same content is never billed twice. Markdown and the JSON-shaped payloads are sent as `text/plain` (the endpoint rejects `text/markdown`); `toc.yml` is sent as a JSON map of its `name` values so hrefs never reach the translator; in `_ui-strings.json` every `{name}` placeholder is encoded as `{{name}}` and decoded on delivery. The status file is saved after every file, so an interrupted run never loses the record of a translation that was already billed and written. A failed request records its file under `failures` with the API error and the run continues with the next file, exiting 1 at the end. -3. **Repair, then verify.** Every delivery is compared with its English source. Whatever can be aligned positionally is restored from English rather than trusted: fenced code blocks (fence lines and bodies), inline code spans, alert and include markers (`[!NOTE]`, `[!include]`), link targets, HTML `href`/`src` attributes, the BOM and the line-ending style, and every frontmatter key except `title`/`description`, which take the translated value (quoted when YAML needs it). What cannot be aligned is a verification problem: heading count differs; fence, marker or link counts differ; HTML tag set differs; JSON key set or per-key placeholder set differs; a `toc.yml` key is missing or a value contains a newline; the source has frontmatter and the delivery does not; or the delivered text is identical to the English source. The one exception is inline code: when the number of backtick spans differs from English, the delivery is still written with the spans as delivered and the drift is reported as a *warning* (`inline code spans 57 -> 59 (missing [...], added [...])`), not as a problem. MT routinely adds or drops a backtick pair around a word like `true` or a variable name; rejecting the delivery would resubmit (and bill) the whole page for one span, and the same MT would do it again on the next delivery. The report says what was repaired (`repaired: n fences, m inline code, k markers, j links`) and lists the files written with warnings. -4. **What "failed" means.** A delivery with problems is *not* written: the existing translation stays untouched, the file is recorded under `failures` in the status file (with the reasons), listed in the report, and not resubmitted until the English source changes or `--retry-failed`/`--force` is given. `--lenient` (local inspection only) writes it anyway, except that empty or otherwise unusable text is never written and `toc.yml`/`_ui-strings.json` deliveries are never written leniently. Warnings are not failures: a delivery whose only finding is inline-code count drift is written and recorded `translated` like a clean one, and listed under *written with warnings* in the report so the reviewer can check the spans; `--baseline` likewise accepts such existing translations as current. +1. **Plan.** Each English file's SHA-256 (line endings normalized) is compared with the entry in `localizedContent/{lang}/.translation-status.json`. Missing and outdated files are submitted. Files with `"manual": true` are skipped and reported as *pinned*. Files whose delivery failed verification at the current source hash are skipped until `--retry-failed` or `--force`; a file whose request failed is planned again. Status entries that are neither in `sources`, `passthrough` nor orphans are reported as *unscoped* and dropped. Translations whose English source no longer exists (*orphans*) are deleted - only when no `--file`/`--limit` filter is active. An outdated file is updated from changed segments when it can be (see [Segment-level updates](#segment-level-updates)); missing and untranslated files, `--force`, HTML files and the fallbacks listed there are sent whole. +2. **Translate.** One synchronous `POST /translate/ai` request per file and language: a file sent whole goes as a single `translatable` segment, a segment-level update as one segment per changed unit (with `target_language`, `content_type` and `service_type`) and the response carries the translation and the billed character count. Every request has an `x-idempotency-id`, reused on retries, so the same content is never billed twice. 408, 425, 429 and 5xx answers and network errors are retried up to three times: after the `Retry-After` time when the answer has one, otherwise after a jittered 1-2, 2-4 and 4-8 seconds (at most 60 seconds). Markdown and the JSON-shaped payloads are sent as `text/plain` (the endpoint rejects `text/markdown`); `toc.yml` is sent as a JSON map of its `name` values so hrefs never reach the translator; in `_ui-strings.json` every `{name}` placeholder is encoded as `{{name}}` and decoded on delivery. In Markdown every fenced code block and inline code span is replaced by a `{{fN}}` / `{{cN}}` placeholder before sending (the service keeps placeholders intact, but rewrites backticks as quotation marks such as «...» or “...”), so code is never sent and never billed. On delivery each placeholder is replaced by its code again and quotation marks the service put around a placeholder are dropped; a missing, repeated or unknown placeholder rejects the delivery. The status file is saved after every file, so an interrupted run never loses the record of a translation that was already billed and written. A failed request records its file under `failures` with the API error (`"request": true`) and the run continues with the next file, exiting 1 at the end; nothing was billed, so the next run sends the file again. A rejected key (401 or 403) stops the run at once and records nothing for the file, with a message that names the `TRANSLATED_API_KEY` secret. +3. **Repair, then verify.** Every delivery is compared with its English source. Whatever can be aligned positionally is restored from English rather than trusted: fenced code blocks (fence lines and bodies), inline code spans, alert and include markers (`[!NOTE]`, `[!include]`), link targets, HTML `href`/`src` attributes, the BOM and the line-ending style, and every frontmatter key except `title`/`description`, which take the translated value (quoted when YAML needs it). Inline code spans, markers, link targets and attributes that are the English values in a different order are left as delivered: the translation moved them with the words around them. What cannot be aligned is a verification problem: heading count differs; fence, marker or link counts differ; HTML tag set differs; JSON key set or per-key placeholder set differs; a `toc.yml` key is missing or a value contains a newline; the source has frontmatter and the delivery does not; or the delivered text is identical to the English source. The one exception is inline code the translation added: when a delivery has more backtick spans than English, it is still written with the spans as delivered and the drift is reported as a *warning* (`inline code spans 57 -> 59 (missing [], added [...])`), not as a problem. MT routinely adds a backtick pair around a word like `true` or a variable name; rejecting the delivery would resubmit (and bill) the whole page for one span. Fewer spans than English is a problem: the code was lost. The report says what was repaired (`repaired: n fences, m inline code, k markers, j links`) and lists the files written with warnings. +4. **What "failed" means.** A delivery with problems is *not* written: the existing translation stays untouched, the file is recorded under `failures` in the status file (with the reasons), listed in the report, and not resubmitted until the English source changes or `--retry-failed`/`--force` is given. The delivery was billed: with `--keep-rejected` it is saved for a manual repair. `--lenient` (local inspection only) writes it anyway, except that empty or otherwise unusable text is never written and `toc.yml`/`_ui-strings.json` deliveries are never written leniently. Warnings are not failures: a delivery whose only finding is added inline code is written and recorded `translated` like a clean one, and listed under *written with warnings* in the report so the reviewer can check the spans; `--baseline` likewise accepts such existing translations as current. 5. **Commit and PR.** The workflow commits `localizedContent` to the target branch and opens the PR, or comments the run report on the open one - also when the translate step itself exited 1 (a failed request), in which case the PR body says so and whatever was translated is still reviewable. Production runs use `localization`; sandbox runs use `localization-sandbox` and open a *draft* PR titled "New translations (sandbox - do not merge)" labelled `sandbox`. The report shows, per language, the files translated, repaired (per kind), written with warnings, copied, failed and removed, plus submitted and billed character counts. `deploy.yml` builds a `localization` PR only when a review is requested, so review it, request a review to get a preview build, then merge. +## Segment-level updates + +When the English of a translated file changes, the run diffs the English the translation was made from against the current English and sends only what changed: + +| File | Unit | Kept from the translation | Sent | Taken from English | +|------|------|---------------------------|------|--------------------| +| Markdown | block (see below), plus frontmatter `title` and `description` | unchanged and moved blocks, blocks whose only change is inline code, a marker, a link target, the list number or the heading level (those parts taken from the new English), unchanged `title`/`description` | changed and new blocks, changed `title`/`description` | changed fenced code, HTML comments, table separator rows, include-only lines, xref-only list items, URL-only lines | +| `_ui-strings.json` | key | unchanged values | changed and new keys | non-string values; removed keys are dropped | +| `toc.yml` | `name` value, matched with its `href` | unchanged and moved entries | changed and new names | hrefs and structure (as for whole files) | + +A Markdown body is split into blocks: a blank line ends a block; a heading, thematic break (`***`) or table row is a block of its own; a list item, a `> [!` alert, a fenced code block and an HTML comment start a new block (`build_scripts/translation_segments.py`). The English version the translation was made from is the git blob recorded in the status entry (`sourceBlob`); its content must hash to `sourceHash`, otherwise the version is looked up by `sourceHash` in the file's git history. The translation must split into the same sequence of block kinds as that English version (it *aligns*); then block *i* of the translation is the translation of block *i* of that English, and unchanged blocks are copied from it with their blank lines. Each copied block is repaired against its own English block only. For a block whose only change is inline code, a marker, a link target, the list number or the heading level, the list marker and heading marks are taken from the new English, and each old English value is replaced by the new value at the same position in the English, wherever the translation placed it; when the old values are ambiguous or the translation lacks one of them, the block is translated. Bookmark anchors that a local build injects before headings (`data-loc-xref`) are removed before the translation is split. + +Each delivered block is repaired and verified like a whole file (fences, inline code, markers, link targets) and must come back as exactly one block of the same kind: a block split by a blank line, a heading delivered as text or a changed heading level is a problem; a changed list marker and the English indentation are restored. The rebuilt page must have the English fence and heading counts and align with the current English, so the next update can diff it again. When nothing translatable changed (fenced code, a removed block or key, or only code, link targets, list numbers and heading levels as above), the file is rebuilt without a request. The written file records the new `sourceHash` and `sourceBlob`; a rejected file keeps both, so the next run diffs from the same version. + +A file is sent whole instead, with the reason in `--plan` and in the report (*translated as a whole file instead of changed segments*), when: + +- the English version the translation was made from is not in git (`English version of the translation not found in git`); +- the translation does not align with it (`unaligned`), for example after a hand edit that added or removed a paragraph; +- the translation does not verify against it (`existing translation does not verify against its English`), for example a link or heading missing in the translation; +- the translation is the English text (`existing translation is the English text`), for example an English fallback copy left by a local `build-docs.py --sync`; +- the changed text exceeds `segmentDiff.maxChangedRatio` of the file's translatable text (`mostly changed`); +- `--retry-failed` retries a file whose segment-level update failed (`retry of a failed segment update`). + +`data[]` of a segment-level response is mapped by position: one entry per sent segment, or one per translatable segment. An echoed `id` must match the segment sent at that position, otherwise the file fails. + ## The workflow (`translate.yml`) Two jobs: -- **`dry_run`** runs on pull requests that touch the workflow, `translate-content.py`, `config_loader.py`, `build-config.json` or `language-metadata.json`. It checks out the PR head and runs `--self-test`, `--plan` and `--dump-orders` (uploaded as an artifact, appended to the job summary). It is fully offline: it never talks to the API, never pushes and never opens a PR. Real sandbox validation is done locally before the PR (see [Local use](#local-use)). -- **`translate`** runs on `workflow_dispatch` always, and on `push` to `main` (paths `content/**`, `metadata/build-config.json`, `metadata/language-metadata.json`, `translate-content.py`, `config_loader.py`, the workflow itself) only when the repository variable `TRANSLATED_ENV` is `production`. Until then the push trigger is inert; there is no schedule - the synchronous endpoint finishes everything in the push-triggered run, and a failed run is recovered with a manual dispatch; a missing key or environment produces a warning, not a red run. Dispatch inputs: `mode` (`translate` | `probe` | `dry-run`), `target_branch` (default empty = auto: `localization` when `TRANSLATED_ENV` is `production`, `localization-sandbox` otherwise; an explicit `localization` is refused outside production in `translate` mode; any other name must start with `localization-`, so the bot can never push to `main` or a feature branch), `limit`, `force` and `file`. A sandbox dispatch must set `limit` or `file`, because the [sandbox caps](#sandbox-caps) refuse an unbounded run. The job first merges the open translation branch into main's tree (after deleting a stale branch whose PR was already merged or closed); a translation branch that cannot be merged stops the run with an error instead of silently continuing from `main`. It then runs `--self-test`, then `--run --limit [--force] [--file ] --report ...`, then pushes and opens or updates the PR; the PR step also runs when the translate step failed, so translations that were already billed and written are committed, with a note in the PR body. In `probe` mode it runs `--probe` only. In `dry-run` mode it runs `--plan` and `--dump-orders`; nothing is submitted, committed or pushed (the AI endpoint bills synchronously, so a run that is never pushed would be paid for and then re-billed by the next real run). The full report is always uploaded as an artifact and appended to the job summary. +- **`dry_run`** runs on pull requests that touch the workflow, `translate-content.py`, `translation_segments.py`, `config_loader.py`, `build-config.json` or `language-metadata.json`. It checks out the PR head with full history (the plan reads the English versions the translations were made from) and runs `--self-test`, `--plan` and `--dump-orders` (uploaded as an artifact, appended to the job summary). It is fully offline: it never talks to the API, never pushes and never opens a PR. Real sandbox validation is done locally before the PR (see [Local use](#local-use)). +- **`translate`** runs on `workflow_dispatch` always, and on a nightly schedule (03:00 UTC) only when the repository variable `TRANSLATED_ENV` is `production`. One scheduled run collects every English change merged that day, so a paragraph edited in several PRs is translated (and billed) once; dispatch the workflow to translate sooner. Until `TRANSLATED_ENV` is `production` the schedule is inert, and a missing key or environment produces a warning, not a red run. Dispatch inputs: `mode` (`translate` | `probe` | `dry-run`), `target_branch` (default empty = auto: `localization` when `TRANSLATED_ENV` is `production`, `localization-sandbox` otherwise; an explicit `localization` is refused outside production in `translate` mode; any other name must start with `localization-`, so the bot can never push to `main` or a feature branch), `limit`, `force` and `file`. A sandbox dispatch must set `limit` or `file`, because the [sandbox caps](#sandbox-caps) refuse an unbounded run. The job first merges the open translation branch into main's tree (after deleting a stale branch whose PR was already merged or closed) and rebuilds the status files entry by entry with `--merge-status`; a translation branch that cannot be merged stops the run with an error instead of silently continuing from `main`. It then runs `--self-test`, then `--run --limit [--force] [--file ] --report ... --keep-rejected ...` (35-minute step timeout, below the job's 45, so a slow run fails the step and the later steps still run), then pushes and opens or updates the PR. The push and PR steps also run when the translate step failed or the run was cancelled, so translations that were already billed and written are committed, with a note in the PR body. Before the push, this run's translations are saved as `translations.patch` (apply with `git apply`): if the push is rejected, for example because a reviewer pushed to the branch during the run, the patch in the artifact still holds them. In `probe` mode it runs `--probe` only. In `dry-run` mode it runs `--plan` and `--dump-orders`; nothing is submitted, committed or pushed (the AI endpoint bills synchronously, so a run that is never pushed would be paid for and then re-billed by the next real run). The report, `translations.patch` and the rejected deliveries are always uploaded as an artifact, and the report is appended to the job summary. -Secrets and settings: `TRANSLATED_API_KEY` (repository secret; the key of the environment `TRANSLATED_ENV` names), `TRANSLATED_ENV` and `TRANSLATED_SERVICE_TYPE` (repository variables), *Allow GitHub Actions to create and approve pull requests* enabled under Settings > Actions > General, and *Automatically delete head branches* enabled under Settings > General. The `translate` job runs with `contents: write`, `pull-requests: write` and `issues: write` (the last one creates the `sandbox` label). +Secrets and settings: `TRANSLATED_API_KEY` (repository secret; the key of the environment `TRANSLATED_ENV` names), `TRANSLATED_ENV`, `TRANSLATED_SERVICE_TYPE` and `TRANSLATED_CONTEXT_SEGMENTS` (repository variables), *Allow GitHub Actions to create and approve pull requests* enabled under Settings > Actions > General, and *Automatically delete head branches* enabled under Settings > General. The `translate` job runs with `contents: write`, `pull-requests: write` and `issues: write` (the last one creates the `sandbox` label). ## Sandbox caps @@ -268,19 +298,19 @@ Local runs use the production account (the sandbox has no AI-enabled service typ ## Baseline -`--baseline` was used once when migrating from the previous provider, and is the way to (re)seed a status file. For every scoped source with an existing translation it runs the same repair-then-verify step a delivery goes through, between English and the existing translation: translations that verify, or whose problems the repair step can fix (fences, inline code, markers, link targets, HTML attributes restored from English), are recorded as `translated` at the English hash; only unrepairable ones are recorded as `untranslated` with an empty hash and the reason is printed, so they are submitted on the first run; missing targets are `untranslated`. By default the repaired text is *not* written - the file is accepted as it is and only the status file changes; pass `--repair-existing` to write the repaired text as well. The English hash is taken from `--baseline-ref REF` when given (via `git show`, so pages edited since that ref are re-translated on the first run); the ref must resolve to a commit or the run exits 1. Unscoped entries are dropped and passthrough entries are left alone. It refuses to run when the status file already has `files` entries unless `--overwrite-baseline` is given, and prints a per-language summary including the files it marked outdated and why. Existing translations whose only finding is inline-code count drift are accepted as current too (printed as `warning` lines and counted as *accepted with inline-code warnings*), for the same reason such deliveries are written: resubmitting a whole page for one backtick pair is not worth it. +`--baseline` was used once when migrating from the previous provider, and is the way to (re)seed a status file. For every scoped source with an existing translation it runs the same repair-then-verify step a delivery goes through, between English and the existing translation: translations that verify, or whose problems the repair step can fix (fences, inline code, markers, link targets, HTML attributes restored from English), are recorded as `translated` at the English hash; only unrepairable ones are recorded as `untranslated` with an empty hash and the reason is printed, so they are submitted on the first run; missing targets are `untranslated`. By default the repaired text is *not* written - the file is accepted as it is and only the status file changes; pass `--repair-existing` to write the repaired text as well. The English hash is taken from `--baseline-ref REF` when given (via `git show`, so pages edited since that ref are re-translated on the first run); the ref must resolve to a commit or the run exits 1. Unscoped entries are dropped and passthrough entries are left alone. It refuses to run when the status file already has `files` entries unless `--overwrite-baseline` is given, and prints a per-language summary including the files it marked outdated and why. Existing translations whose only finding is added inline code are accepted as current too (printed as `warning` lines and counted as *accepted with inline-code warnings*), for the same reason such deliveries are written: resubmitting a whole page for one backtick pair is not worth it. ## Status file -`localizedContent/{lang}/.translation-status.json` is owned by `translate-content.py` and committed with the translations. It holds `language`, `sourceBaseline`, `files` (per file: `sourceHash`, `status` = `translated` | `untranslated` | `copied`, and optionally `"manual": true`), `failures` (present only when non-empty), and `summary` (counts including `copied`, `pinned`, `failures`). `build-docs.py --sync` reads it but never writes it. (A legacy `pendingJobs` section from the earlier asynchronous flow is dropped on the next run; the AI endpoint leaves nothing pending.) +`localizedContent/{lang}/.translation-status.json` is owned by `translate-content.py` and committed with the translations. It holds `language`, `sourceBaseline`, `files` (per file: `sourceHash`, `sourceBlob` = the git blob of that English version when it is in git, `status` = `translated` | `untranslated` | `copied`, and optionally `"manual": true`), `failures` (present only when non-empty), and `summary` (counts including `copied`, `pinned`, `failures`). `build-docs.py --sync` reads it but never writes it. (A legacy `pendingJobs` section from the earlier asynchronous flow is dropped on the next run; the AI endpoint leaves nothing pending.) ## Runbook -- **Fix a translation.** While the translation PR is open, push the fix to its branch (`localization`); the next run merges that branch before doing anything else, so the fix survives. After the PR is merged, edit the file on `main` through a normal PR. Either way the file is overwritten by the next machine translation when its English source changes, unless you pin it. +- **Fix a translation.** While the translation PR is open, push the fix to its branch (`localization`); the next run merges that branch before doing anything else, so the fix survives. After the PR is merged, edit the file on `main` through a normal PR. Either way the fix is kept until the English of that block changes. A fix that adds or removes a block makes the file unaligned, so its next English change sends the whole file; pin the file to keep such a fix. - **Pin (skip) a file.** Add `"manual": true` to the file's entry in `localizedContent/{lang}/.translation-status.json`. Pinned files are reported and never submitted or overwritten; remove the flag to hand the file back to the workflow. - **Re-translate one file.** Dispatch `translate.yml` with `force=true` and `file=features/dax-query.md` (and `limit` as needed), or locally: `python build_scripts/translate-content.py --run --force --file "features/dax-query.md" --lang es --report r.md`. Local runs use whatever `TRANSLATED_ENV` says; only commit output from production. - **A file failed.** The report and the `failures` entry list the reasons (for example "heading count differs" or "delivered content identical to source"). Usually the English source has a structure the translator cannot preserve: fix the English, and the changed hash resubmits the file on the next run (or use `--retry-failed` to try again unchanged). If the delivery is acceptable despite the problems, fetch it locally with `--run --force --file --lang --lenient --report r.md`, review the written file, and commit it through a normal PR. -- **Go live on production.** Both environments default to service type `AI` in `build-config.json`; set the repository variable `TRANSLATED_SERVICE_TYPE` only to override it. (1) Set the `TRANSLATED_API_KEY` secret to the production key. (2) Confirm the key, the locales and the `AI` service type locally first: `TRANSLATED_ENV=production TRANSLATED_API_KEY= python build_scripts/translate-content.py --probe` (read-only; in PowerShell set `$env:TRANSLATED_ENV` and `$env:TRANSLATED_API_KEY` first), optionally followed by a one-file billed test run (see [Local use](#local-use)). (3) Set the repository variable `TRANSLATED_ENV` to `production`; from now on the automatic triggers are live. (4) Dispatch `translate.yml` with `mode=probe` to confirm the same from the workflow's secrets and variables. (5) Dispatch `mode=translate` with `limit=3`, `force=true`, `file=features/toc.md` and `target_branch` empty (auto = `localization`) for the first reviewed PR; request a review to get a preview build, merge. (6) Let the push trigger take over; watch the first couple of content merges. +- **Go live on production.** Both environments default to service type `AI` in `build-config.json`; set the repository variable `TRANSLATED_SERVICE_TYPE` only to override it. (1) Set the `TRANSLATED_API_KEY` secret to the production key. (2) Confirm the key, the locales and the `AI` service type locally first: `TRANSLATED_ENV=production TRANSLATED_API_KEY= python build_scripts/translate-content.py --probe` (read-only; in PowerShell set `$env:TRANSLATED_ENV` and `$env:TRANSLATED_API_KEY` first), optionally followed by a one-file billed test run (see [Local use](#local-use)). (3) Set the repository variable `TRANSLATED_ENV` to `production`; from now on the nightly schedule is live. (4) Dispatch `translate.yml` with `mode=probe` to confirm the same from the workflow's secrets and variables. (5) Dispatch `mode=translate` with `limit=3`, `force=true`, `file=features/toc.md` and `target_branch` empty (auto = `localization`) for the first reviewed PR; request a review to get a preview build, merge. (6) Let the nightly schedule take over; watch the first couple of runs. - **Stale-branch cleanup.** Before merging the translation branch into its tree, the workflow checks whether the branch tip is already contained in `main` or its PR is merged or closed (`gh pr list --head --state merged` / `--state closed`). Closing the translation PR - merged or not - marks the branch stale: the next run deletes the remote branch and starts from `main`, so a squash-merged translation PR never produces an empty follow-up PR (*Automatically delete head branches* makes this the normal path). Translations that lived only on that branch are lost with it, so those files are retranslated (and billed again) on the next run. To keep unmerged translations, leave the PR open or reopen it before the next run. # Bookmark Links and Translations diff --git a/build_scripts/translate-content.py b/build_scripts/translate-content.py index 018106ac4..a00783a63 100644 --- a/build_scripts/translate-content.py +++ b/build_scripts/translate-content.py @@ -8,10 +8,18 @@ per character), so nothing is pending between runs. Every delivery is repaired against the English source (code, markers, link targets restored), verified, and written in the same step. +An outdated translation is updated from changed segments only: the English version it was made +from (status `sourceBlob`, verified against `sourceHash`) is diffed block by block against the +current English (translation_segments.py), unchanged blocks are kept from the translation and +only changed blocks, JSON keys or toc names are sent. Missing files, unaligned translations and +files whose text mostly changed are sent whole. + Actions (exactly one per invocation): --plan list missing/outdated/orphaned/pinned/unscoped files (offline) --run translate every missing/outdated file, verify and write the results --baseline seed the status file from the existing translations (offline) + --migrate-blobs record sourceBlob for status entries that only have a sourceHash (offline) + --merge-status OURS THEIRS rebuild the status files entry by entry after merging a translation branch (offline) --self-test offline test suite (no key, no network) --probe check service types and languages against the API --dump-orders write the exact request bodies to a directory instead of sending @@ -22,23 +30,28 @@ Environment: TRANSLATED_ENV (sandbox | production; the sandbox account has no AI-enabled service type, so only production can run), TRANSLATED_API_KEY, and -TRANSLATED_SERVICE_TYPE (optional override; both environments default to 'AI'). - -Exit code 1 on configuration errors or failed API requests. Files that fail verification do -not fail the run: they are recorded under "failures" in the status file and skipped until -the English changes or --retry-failed. Full reference: README.md > Translating Content. +TRANSLATED_SERVICE_TYPE (optional override; both environments default to 'AI') and +TRANSLATED_CONTEXT_SEGMENTS (optional override of translation.segmentDiff.contextSegments). + +Exit code 1 on configuration errors or failed API requests. A rejected API key (401/403) stops +the run and records nothing for the file. Any other failed request is recorded under "failures" +and planned again by the next run. Files whose delivery fails verification do not fail the run: +they are recorded under "failures" and skipped until the English changes or --retry-failed, and +--keep-rejected saves the billed delivery. Full reference: README.md > Translating Content. """ from __future__ import annotations import argparse import difflib +import email.utils import fnmatch import hashlib import http.client import io import json import os +import random import re import shutil import subprocess @@ -57,6 +70,21 @@ from typing import Any, Protocol from config_loader import compute_file_hash, get_default_language, load_build_config +from translation_segments import ( + ACTION_ENGLISH, + ACTION_REPAIR, + ACTION_REUSE, + ACTION_TRANSLATE, + KIND_HEADING, + KIND_LIST_ITEM, + LIST_ITEM_RE, + Action, + Block, + context_indices, + diff_blocks, + reused_layout, + segment, +) CONTENT_DIR = Path("content") LOCALIZED_DIR = Path("localizedContent") @@ -70,7 +98,9 @@ TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ" -RETRYABLE_HTTP = frozenset({425, 429, 500, 502, 503, 504}) +RETRYABLE_HTTP = frozenset({408, 425, 429, 500, 502, 503, 504}) +AUTH_HTTP = frozenset({401, 403}) +MAX_RETRY_DELAY = 60.0 REPORT_LIST_CAP = 50 IDENTITY_MIN_LETTERS = 200 REPAIR_KINDS = ("fences", "inline code", "markers", "links") # order used in "repaired: ..." lines @@ -88,10 +118,12 @@ } BOM = "" +WHOLE_FILE_TYPE = "file type is translated whole" # Markdown structure regexes (mirror normalize-localized-heading-anchors.py). FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})") HEADING_RE = re.compile(r"^#{1,6}\s+\S") +HEADING_MARKS_RE = re.compile(r"^#{1,6}(?=[ \t])") # Any `[!word` token: alert markers ([!NOTE]), includes ([!include[...]) and code refs ([!code-yaml[...]). # Deliberately broad so a translated marker ([!NOTA]) is still counted and restored positionally. ALERT_RE = re.compile(r"\[![A-Za-z][A-Za-z0-9-]*") @@ -106,6 +138,13 @@ YAML_SCALARS = frozenset({"true", "false", "yes", "no", "null", "on", "off", "~"}) HTML_TAG_RE = re.compile(r"<\s*([a-zA-Z][a-zA-Z0-9-]*)") HTML_REF_RE = re.compile(r"""\b(?:href|src)\s*=\s*["']([^"']+)["']""") +# Bookmark anchors that build-docs (normalize-localized-heading-anchors.py) injects into a local tree. +INJECTED_ANCHOR_RE = re.compile(r'^\n', re.M) +TOC_HREF_RE = re.compile(r"^\s*href:[ \t]*(.*?)[ \t]*$") +# {{cN}} (inline code) and {{fN}} (fenced block) placeholders; quotation marks the service puts +# around one are matched so they can be dropped. +CODE_TOKEN_RE = re.compile(r"([«“\"‘「『]?)\{\{\s*([cf])(\d+)\s*\}\}([»”\"’」』]?)") +QUOTE_PAIRS = {"«": "»", "“": "”", '"': '"', "‘": "’", "「": "」", "『": "』"} PLACEHOLDER_RE = re.compile(r"(? None: section = build_config.get("translation") if not section: @@ -158,6 +198,17 @@ def __init__( limits = section.get("sandboxLimits") or {} self.max_files_per_run: int = int(limits.get("maxFilesPerRun", 20)) self.max_chars_per_run: int = int(limits.get("maxCharsPerRun", 200000)) + segment_diff = section.get("segmentDiff") or {} + self.segment_diff: bool = bool(segment_diff.get("enabled", True)) + self.max_changed_ratio: float = float(segment_diff.get("maxChangedRatio", 0.8)) + context = (context_segments_override or "").strip() or segment_diff.get("contextSegments", 2) + try: + self.context_segments: int = int(context) + except ValueError: + raise SystemExit( + f"Context segments must be an integer (-1 = whole page, 0 = none), got {context!r} " + "(TRANSLATED_CONTEXT_SEGMENTS or translation.segmentDiff.contextSegments)." + ) from None self.shared_directories: list[str] = list(build_config.get("sharedDirectories", {}).get("directories", [])) if not self.sources: raise SystemExit("translation.sources in metadata/build-config.json is empty.") @@ -282,6 +333,96 @@ def hashes_at_git_ref(ref: str, rel_paths: list[str], run: GitRunner = run_git) return result +_WORK_TREE: dict[str, bool] = {} + + +def in_git_work_tree(run: GitRunner = run_git) -> bool: + cwd = os.getcwd() + if run is run_git and cwd in _WORK_TREE: + return _WORK_TREE[cwd] + proc = run(["git", "rev-parse", "--is-inside-work-tree"]) + inside = proc.returncode == 0 and proc.stdout.strip() == b"true" + if run is run_git: + _WORK_TREE[cwd] = inside + return inside + + +def read_git_blob(blob: str, run: GitRunner = run_git) -> bytes | None: + proc = run(["git", "cat-file", "blob", blob]) + return proc.stdout if proc.returncode == 0 else None + + +def git_stdout(proc: subprocess.CompletedProcess[bytes]) -> str | None: + out = proc.stdout.decode("utf-8", errors="replace").strip() + return out if proc.returncode == 0 and out else None + + +def current_source_blob(rel: str, source_hash: str, run: GitRunner = run_git) -> str | None: + """Git blob id of content/{rel} when the file's hash is `source_hash`; None outside a git work tree.""" + path = CONTENT_DIR / rel + if not path.exists() or hash_bytes(path.read_bytes()) != source_hash or not in_git_work_tree(run): + return None + # -w stores the blob, so an uncommitted English version stays readable as the base of the next diff. + return git_stdout(run(["git", "hash-object", "-w", "--", path.as_posix()])) + + +def blob_at_git_ref(sha: str, rel: str, run: GitRunner = run_git) -> str | None: + return git_stdout(run(["git", "rev-parse", "--verify", "--quiet", f"{sha}:{CONTENT_DIR.as_posix()}/{rel}"])) + + +def find_source_blob(rel: str, source_hash: str, run: GitRunner = run_git) -> tuple[str, bytes] | None: + """The English version of content/{rel} whose hash is `source_hash`: the working file when it + matches, else the newest committed version that does. Returns (blob id, content).""" + if not source_hash: + return None + current = current_source_blob(rel, source_hash, run) + if current: + return current, (CONTENT_DIR / rel).read_bytes() + if not in_git_work_tree(run): + return None + proc = run(["git", "log", "--no-abbrev", "--format=", "--raw", "--", f"{CONTENT_DIR.as_posix()}/{rel}"]) + if proc.returncode != 0: + return None + seen: set[str] = set() + for line in proc.stdout.decode("utf-8", errors="replace").splitlines(): + # :100644 100644 Mpath + parts = line.split() + if len(parts) < 5 or not parts[0].startswith(":") or not parts[3].strip("0") or parts[3] in seen: + continue + seen.add(parts[3]) + data = read_git_blob(parts[3], run) + if data is not None and hash_bytes(data) == source_hash: + return parts[3], data + return None + + +def recorded_source_text(rel: str, entry: dict[str, Any], run: GitRunner = run_git) -> str | None: + """The English text an existing translation was made from: the entry's sourceBlob when its + content still hashes to sourceHash, else the version found by find_source_blob.""" + source_hash = str(entry.get("sourceHash") or "") + if not source_hash: + return None + blob = entry.get("sourceBlob") + if isinstance(blob, str) and blob: + data = read_git_blob(blob, run) + if data is not None and hash_bytes(data) == source_hash: + return data.decode("utf-8") + found = find_source_blob(rel, source_hash, run) + return found[1].decode("utf-8") if found else None + + +def source_entry( + rel: str, source_hash: str, status_value: str, run: GitRunner = run_git, blob: str | None = None +) -> dict[str, Any]: + """A status entry; sourceBlob is recorded when the English version is in git (the base of the next diff).""" + entry: dict[str, Any] = {"sourceHash": source_hash} + blob = blob or current_source_blob(rel, source_hash, run) + if blob: + entry["sourceBlob"] = blob + entry["status"] = status_value + return entry + + def matches_filters(rel: str, file_filters: list[str]) -> bool: return not file_filters or any(fnmatch.fnmatch(rel, pat) for pat in file_filters) @@ -456,6 +597,56 @@ def decode_placeholders(text: str) -> str: return ENCODED_PLACEHOLDER_RE.sub(r"{\1}", text) +def mask_code(text: str, fences: bool = True) -> tuple[str, list[str]]: + """Replace fenced code blocks (when `fences`) and inline code spans with {{fN}} / {{cN}} + placeholders. The service keeps such placeholders intact; it rewrites backticks as quotation + marks. Returns (masked text, the original code by N).""" + spans: list[str] = [] + + def token(kind: str, value: str) -> str: + spans.append(value) + return "{{" + f"{kind}{len(spans) - 1}" + "}}" + + out: list[str] = [] + for seg, is_fence in split_fences(text) if fences else [(text, False)]: + if is_fence: + block = seg[:-1] if seg.endswith("\n") else seg + out.append(token("f", block) + seg[len(block) :]) + else: + out.append(INLINE_CODE_RE.sub(lambda m: token("c", m.group(0)), seg)) + return "".join(out), spans + + +def unmask_code(text: str, spans: list[str]) -> tuple[str, list[str]]: + """Put the original code back for every placeholder, dropping quotation marks the service put + around one. Returns (text, problems): placeholders missing, repeated or unknown.""" + seen: Counter[int] = Counter() + unknown: list[str] = [] + + def put(m: re.Match[str]) -> str: + n = int(m.group(3)) + if n >= len(spans): + unknown.append(m.group(0)) + return m.group(0) + seen[n] += 1 + opening, closing = m.group(1), m.group(4) + if opening and QUOTE_PAIRS.get(opening) == closing: + opening = closing = "" + return opening + spans[n] + closing + + text = CODE_TOKEN_RE.sub(put, text) + problems: list[str] = [] + missing = [n for n in range(len(spans)) if not seen[n]] + repeated = sorted(n for n, k in seen.items() if k > 1) + if missing: + problems.append(f"code placeholders missing in the delivery: {missing[:5]} ({len(missing)} of {len(spans)})") + if repeated: + problems.append(f"code placeholders repeated in the delivery: {repeated[:5]}") + if unknown: + problems.append(f"unknown code placeholders in the delivery: {unknown[:5]}") + return text, problems + + def prepare_payload(rel: str, source_text: str) -> tuple[str, str, dict[str, Any]]: """Return (content, content_type, extra job info) for a source file.""" suffix = Path(rel).suffix.lower() @@ -470,6 +661,9 @@ def prepare_payload(rel: str, source_text: str) -> tuple[str, str, dict[str, Any content = to_lf(source_text.lstrip(BOM)) if suffix == ".json": return encode_placeholders(content), content_type, {"mode": "json-placeholders"} + if suffix == ".md": + masked, spans = mask_code(content) + return masked, content_type, {"mode": "raw", "codeSpans": spans} return content, content_type, {"mode": "raw"} @@ -550,11 +744,14 @@ def count_fence_lines(body: str) -> int: def restore_positionally(text: str, source_text: str, regex: re.Pattern[str], group: int) -> tuple[str, int] | None: """Replace every match of `regex` (its `group`) in `text` with the source's value at the - same position. Returns (text, changed) when the counts match, None otherwise.""" + same position. Returns (text, changed) when the counts match, None otherwise. Values that + are the source's values in another order are left as delivered (word order differs by language).""" src_values = [m.group(group) for m in regex.finditer(source_text)] matches = list(regex.finditer(text)) if len(src_values) != len(matches): return None + if Counter(src_values) == Counter(m.group(group) for m in matches): + return text, 0 out: list[str] = [] pos = 0 changed = 0 @@ -614,6 +811,16 @@ class RepairResult: ) +def prose_key(text: str) -> str: + """The text with everything the repair step restores from English, a list marker and heading marks, + masked out.""" + text = LIST_ITEM_RE.sub("\x00", text, count=1) + text = HEADING_MARKS_RE.sub("\x00", text, count=1) + for regex, _group, _kind, _what in MD_RESTORE_RULES: + text = regex.sub("\x00", text) + return text + + def repair_markdown_body(source_body: str, translated_body: str, repair: bool = True) -> RepairResult: """Repair-then-verify for a Markdown body (LF line endings, no frontmatter). Fenced blocks, inline code, alert/include markers and link targets are restored from @@ -640,7 +847,8 @@ def repair_markdown_body(source_body: str, translated_body: str, repair: bool = for regex, group, kind, what in MD_RESTORE_RULES: restored = restore_positionally(tr_text, src_text, regex, group) if restored is None: - findings = result.warnings if kind in WARNING_KINDS else result.problems + lost = sum(1 for _ in regex.finditer(tr_text)) < sum(1 for _ in regex.finditer(src_text)) + findings = result.warnings if kind in WARNING_KINDS and not lost else result.problems findings.append(count_diff(src_text, tr_text, regex, group, what)) elif restored[1] and repair: result.repaired[kind] += restored[1] @@ -788,6 +996,422 @@ def finalize_translation( return bom + text, problems, repaired, warnings +# --------------------------------------------------------------------------- +# Segment-level updates (only what changed since the English the translation was made from) +# --------------------------------------------------------------------------- + +Assembler = Callable[[dict[str, str]], tuple[str, list[str], Counter[str], list[str]]] + + +@dataclass +class Delta: + """The content[] items of a segment-level order and how to rebuild the file from the delivery. + `assemble` takes {segment id -> translation} and returns (text, problems, repaired, warnings).""" + + mode: str + items: list[dict[str, Any]] + assemble: Assembler + changed: int # translatable units sent + total: int # translatable units in the file + changed_chars: int + total_chars: int + + @property + def chars(self) -> int: + return sum(len(str(i["text"])) for i in self.items if i["translatable"]) + + +def unquote_scalar(value: str) -> str: + v = value.strip() + if len(v) >= 2 and v[0] == v[-1] == '"': + try: + return str(json.loads(v)) + except json.JSONDecodeError: + return v + if len(v) >= 2 and v[0] == v[-1] == "'": + return v[1:-1].replace("''", "'") + return v + + +def newline_eol(text: str) -> tuple[str, str, bool]: + """(BOM, LF text without BOM, CRLF?) of a source file.""" + return (BOM if text.startswith(BOM) else ""), to_lf(text.lstrip(BOM)), "\r\n" in text + + +def match_block_end(text: str, english: str) -> str: + """`text` with the trailing newline of its English block (a reused last block may lack one).""" + if english.endswith("\n") and not text.endswith("\n"): + return text + "\n" + if not english.endswith("\n") and text.endswith("\n"): + return text.rstrip("\n") + return text + + +def restore_list_marker(english: str, text: str) -> tuple[str, bool]: + source_marker = LIST_ITEM_RE.match(english) + delivered_marker = LIST_ITEM_RE.match(text) + if source_marker and delivered_marker and source_marker.group(0) != delivered_marker.group(0): + return source_marker.group(0) + text[delivered_marker.end() :], True + return text, False + + +def restore_heading_marks(english: str, text: str) -> str: + source_marks = HEADING_MARKS_RE.match(english) + marks = HEADING_MARKS_RE.match(text) + if source_marks and marks and source_marks.group(0) != marks.group(0): + return source_marks.group(0) + text[marks.end() :] + return text + + +def repair_by_value(old: str, new: str, loc: str) -> str | None: + """`loc` (the translation of `old`) with every code span, marker and link target replaced by its + counterpart in `new`: the old value at position k becomes the new value at position k, wherever + the translation placed it. None when one old value maps to two new values or the translation's + spans are not the old spans.""" + text = loc + for regex, group, _kind, _what in MD_RESTORE_RULES: + old_values = [m.group(group) for m in regex.finditer(old)] + new_values = [m.group(group) for m in regex.finditer(new)] + if len(old_values) != len(new_values): + return None + mapping: dict[str, str] = {} + for o, n in zip(old_values, new_values, strict=True): + if mapping.setdefault(o, n) != n: + return None + matches = list(regex.finditer(text)) + if Counter(m.group(group) for m in matches) != Counter(old_values): + return None + out: list[str] = [] + pos = 0 + for m in matches: + start, end = m.span(group) + out.append(text[pos:start]) + out.append(mapping[m.group(group)]) + pos = end + out.append(text[pos:]) + text = "".join(out) + return text + + +def finalize_block(block: Block, delivered: str) -> tuple[str, list[str], Counter[str], list[str]]: + """Repair and verify one delivered Markdown block against its English block. The delivery must + segment as exactly one block of the same kind, so the page keeps aligning with the English. + Returns (text, problems, repaired, warnings).""" + lines = to_lf(delivered.lstrip(BOM)).split("\n") + while lines and not lines[0].strip(): + lines.pop(0) + while lines and not lines[-1].strip(): + lines.pop() + indent = block.text[: len(block.text) - len(block.text.lstrip(" \t"))] + if indent and block.kind != KIND_LIST_ITEM: + lines = [line if line.startswith(indent) else indent + line.lstrip(" \t") for line in lines] + text = match_block_end("\n".join(lines), block.text) + seg = segment(text) + if seg.lead or len(seg.blocks) != 1 or seg.blocks[0].kind != block.kind or seg.blocks[0].gap: + return text, [f"{block.kind} block came back as {' + '.join(seg.kinds) or 'nothing'}"], Counter(), [] + repaired: Counter[str] = Counter() + if block.kind == KIND_HEADING: + marks, delivered_marks = block.text.split(None, 1)[0], text.split(None, 1)[0] + if marks != delivered_marks: + return text, [f"heading level {marks} -> {delivered_marks}"], Counter(), [] + if block.kind == KIND_LIST_ITEM: + text, changed = restore_list_marker(block.text, text) + repaired["markers"] += changed + result = repair_markdown_body(block.text, text) + repaired.update(result.repaired) + return result.text, result.problems, +repaired, result.warnings + + +def heading_count(body: str) -> int: + return sum(1 for line in markdown_prose(body).splitlines() if HEADING_RE.match(line)) + + +def md_delta(new_text: str, old_text: str, loc_text: str, context_width: int = 0) -> Delta | str: + bom, new_lf, crlf = newline_eol(new_text) + new_fm, new_body = split_frontmatter(new_lf) + old_fm, old_body = split_frontmatter(to_lf(old_text.lstrip(BOM))) + loc_fm, loc_body = split_frontmatter(INJECTED_ANCHOR_RE.sub("", to_lf(loc_text.lstrip(BOM)))) + new_seg, old_seg, loc_seg = segment(new_body), segment(old_body), segment(loc_body) + if old_seg.kinds != loc_seg.kinds: + return f"unaligned ({len(old_seg.blocks)} English blocks, {len(loc_seg.blocks)} translated)" + loc_prose = markdown_prose(loc_body) + translatable_letters = sum(letters(b.text) for b in loc_seg.blocks if b.translatable) + if translatable_letters > IDENTITY_MIN_LETTERS and loc_prose in ( + markdown_prose(old_body), + markdown_prose(new_body), + ): + return "existing translation is the English text" + existing = repair_markdown_body(old_body, loc_body) + if existing.problems: + return f"existing translation does not verify against its English ({existing.problems[0]})" + actions = diff_blocks(old_seg.blocks, new_seg.blocks, prose_key) + repaired_blocks: dict[int, str] = {} + for j, a in enumerate(actions): + if a.kind == ACTION_REPAIR: + b = new_seg.blocks[j] + fixed = repair_by_value(old_seg.blocks[a.old_index].text, b.text, loc_seg.blocks[a.old_index].text) + if fixed is None: + actions[j] = Action(ACTION_TRANSLATE) + elif b.kind == KIND_LIST_ITEM: + repaired_blocks[j] = restore_list_marker(b.text, fixed)[0] + else: + repaired_blocks[j] = restore_heading_marks(b.text, fixed) if b.kind == KIND_HEADING else fixed + new_vals, old_vals, loc_vals = frontmatter_values(new_fm), frontmatter_values(old_fm), frontmatter_values(loc_fm) + reused_fm = {k: loc_vals[k] for k, v in new_vals.items() if old_vals.get(k) == v and k in loc_vals} + masks: dict[str, list[str]] = {} + + def item(item_id: str, text: str, translatable: bool) -> dict[str, Any]: + masked, masks[item_id] = mask_code(text, fences=False) + return {"id": item_id, "text": masked, "translatable": translatable} + + def delivered(item_id: str, translations: dict[str, str]) -> tuple[str | None, list[str]]: + if item_id not in translations: + return None, [] + text, mask_problems = unmask_code(translations[item_id], masks[item_id]) + return (None if mask_problems else text), mask_problems + + items: list[dict[str, Any]] = [ + item(f"fm:{k}", unquote_scalar(v), True) for k, v in new_vals.items() if k not in reused_fm + ] + changed = {j for j, a in enumerate(actions) if a.kind == ACTION_TRANSLATE} + context = set(context_indices(actions, new_seg.blocks, context_width)) + for j, b in enumerate(new_seg.blocks): + if j in changed or j in context: + items.append(item(f"{'b' if j in changed else 'c'}{j}", b.text.rstrip("\n"), j in changed)) + translatable_blocks = [b for b in new_seg.blocks if b.translatable] + + def assemble(translations: dict[str, str]) -> tuple[str, list[str], Counter[str], list[str]]: + problems: list[str] = [] + warnings: list[str] = [] + repaired: Counter[str] = Counter() + texts: list[str] = [] + pairs: list[tuple[str, str]] = [] + for j, (b, a) in enumerate(zip(new_seg.blocks, actions, strict=True)): + if a.kind == ACTION_ENGLISH: + text = b.text + elif a.kind == ACTION_REPAIR: + text = repaired_blocks[j] + elif a.kind == ACTION_REUSE: + # Restored against its own English block only: a page-wide positional pass would move + # spans across blocks when one block's count drifts. + text = loc_seg.blocks[a.old_index].text + fixed = repair_markdown_body(b.text, text) + if not fixed.problems: + text = fixed.text + repaired.update(fixed.repaired) + elif (block := delivered(f"b{j}", translations))[0] is None: + problems.extend(f"block {j}: {p}" for p in block[1] or ["missing in the delivery"]) + text = b.text + else: + text, block_problems, block_repaired, block_warnings = finalize_block(b, str(block[0])) + problems.extend(f"block {j}: {p}" for p in block_problems) + warnings.extend(f"block {j}: {w}" for w in block_warnings) + repaired.update(block_repaired) + pairs.append((b.text, text)) + texts.append(match_block_end(text, b.text)) + lead, gaps = reused_layout(actions, new_seg, len(old_seg.blocks), loc_seg) + body = new_seg.join(texts, gaps, lead) + if count_fence_lines(body) != count_fence_lines(new_body): + problems.append(f"code fences {count_fence_lines(new_body)} -> {count_fence_lines(body)}") + if heading_count(body) != heading_count(new_body): + problems.append(f"headings {heading_count(new_body)} -> {heading_count(body)}") + if segment(body).kinds != new_seg.kinds: + problems.append("delivered blocks change the page structure") + if pairs and sum(letters(e) for e, _ in pairs) > IDENTITY_MIN_LETTERS and all(e == t for e, t in pairs): + problems.append("delivered content identical to source") + fm = "" + if new_fm: + # merge_frontmatter reports a value that is missing here (or held a line break) as not found. + values = dict(reused_fm) + for k in new_vals: + if k in reused_fm: + continue + value, value_problems = delivered(f"fm:{k}", translations) + problems.extend(f"frontmatter '{k}': {p}" for p in value_problems) + if value and value.strip() and "\n" not in value.strip(): + values[k] = value.strip() + synthetic = "---\n" + "".join(f"{k}: {v}\n" for k, v in values.items()) + "---\n" + fm, fm_problems = merge_frontmatter(new_fm, synthetic) + problems.extend(fm_problems) + text = fm + body + if crlf: + text = text.replace("\n", "\r\n") + return bom + text, problems, repaired, warnings + + sent = [i for i in items if i["translatable"]] + return Delta( + "md-blocks", + items, + assemble, + changed=len(sent), + total=len(new_vals) + len(translatable_blocks), + changed_chars=sum(len(str(i["text"])) for i in sent), + total_chars=sum(len(unquote_scalar(v)) for v in new_vals.values()) + + sum(len(b.text.rstrip("\n")) for b in translatable_blocks), + ) + + +def json_delta(new_text: str, old_text: str, loc_text: str) -> Delta | str: + bom, new_lf, crlf = newline_eol(new_text) + new, old, loc = (json.loads(to_lf(t.lstrip(BOM))) for t in (new_text, old_text, loc_text)) + if not all(isinstance(x, dict) for x in (new, old, loc)): + return "unaligned (JSON root is not an object)" + if loc in (old, new): + return "existing translation is the English text" + reuse = {k for k, v in new.items() if isinstance(v, str) and old.get(k) == v and isinstance(loc.get(k), str)} + changed = [k for k, v in new.items() if isinstance(v, str) and k not in reuse] + indent = json_indent(new_lf) + subset = json.dumps({k: new[k] for k in changed}, indent=indent, ensure_ascii=False) + "\n" + items = [{"id": "json", "text": encode_placeholders(subset), "translatable": True}] if changed else [] + + def assemble(translations: dict[str, str]) -> tuple[str, list[str], Counter[str], list[str]]: + translated: dict[str, Any] = {} + if changed: + if "json" not in translations: + return "", ["no translation in the response"], Counter(), [] + text, problems = verify_json_strings(subset, translations["json"]) + if problems: + return "", problems, Counter(), [] + translated = json.loads(text) + out = {k: translated[k] if k in translated else loc[k] if k in reuse else v for k, v in new.items()} + text = json.dumps(out, indent=indent, ensure_ascii=False) + ("\n" if new_lf.endswith("\n") else "") + if crlf: + text = text.replace("\n", "\r\n") + return bom + text, [], Counter(), [] + + strings = [v for v in new.values() if isinstance(v, str)] + return Delta( + "json-keys", + items, + assemble, + changed=len(changed), + total=len(strings), + changed_chars=sum(len(new[k]) for k in changed), + total_chars=sum(len(v) for v in strings), + ) + + +def toc_entry_keys(text: str, names: dict[str, str]) -> dict[str, tuple[str, str]]: + """{name key -> (name, href on the next line)}: an entry is matched by both, so two entries + with the same English name keep their own translations when they move.""" + lines = to_lf(text.lstrip(BOM)).split("\n") + keys: dict[str, tuple[str, str]] = {} + for key, name in names.items(): + i = int(key[1:]) + 1 + m = TOC_HREF_RE.match(lines[i]) if i < len(lines) else None + keys[key] = (name, m.group(1) if m else "") + return keys + + +def toc_delta(new_text: str, old_text: str, loc_text: str) -> Delta | str: + new_names, old_names, loc_names = yaml_name_map(new_text), yaml_name_map(old_text), yaml_name_map(loc_text) + if list(old_names) != list(loc_names): + return f"unaligned ({len(old_names)} English names, {len(loc_names)} translated)" + if list(loc_names.values()) in (list(old_names.values()), list(new_names.values())): + return "existing translation is the English text" + old_entries, new_entries = toc_entry_keys(old_text, old_names), toc_entry_keys(new_text, new_names) + old_keys, new_keys = list(old_names), list(new_names) + first_key: dict[tuple[str, str], str] = {} + for k in old_keys: + first_key.setdefault(old_entries[k], k) + matcher = difflib.SequenceMatcher( + None, [old_entries[k] for k in old_keys], [new_entries[k] for k in new_keys], autojunk=False + ) + reused: dict[str, str] = {} + for tag, i1, _i2, j1, j2 in matcher.get_opcodes(): + for j in range(j1, j2): + key = new_keys[j] + if tag == "equal": + reused[key] = loc_names[old_keys[i1 + (j - j1)]] + elif new_entries[key] in first_key: + reused[key] = loc_names[first_key[new_entries[key]]] + changed = [k for k in new_keys if k not in reused] + payload = json.dumps({k: new_names[k] for k in changed}, ensure_ascii=False, indent=2) + items = [{"id": "toc", "text": payload, "translatable": True}] if changed else [] + + def assemble(translations: dict[str, str]) -> tuple[str, list[str], Counter[str], list[str]]: + names = dict(reused) + if changed: + try: + delivered = json.loads(translations.get("toc", "").lstrip(BOM)) + except json.JSONDecodeError as e: + return new_text, [f"translated toc payload does not parse: {e}"], Counter(), [] + if not isinstance(delivered, dict) or set(delivered) != set(changed): + got = sorted(delivered) if isinstance(delivered, dict) else type(delivered).__name__ + return ( + new_text, + [f"toc payload keys differ: expected {changed[:5]}, got {str(got)[:120]}"], + Counter(), + [], + ) + names.update({k: str(v) for k, v in delivered.items()}) + text, problems = reinsert_yaml_names(new_text, json.dumps(names, ensure_ascii=False)) + return text, problems, Counter(), [] + + return Delta( + "toc-names", + items, + assemble, + changed=len(changed), + total=len(new_keys), + changed_chars=sum(len(new_names[k]) for k in changed), + total_chars=sum(len(v) for v in new_names.values()), + ) + + +def plan_delta( + config: TranslationConfig, lang: str, rel: str, entry: dict[str, Any], run: GitRunner = run_git +) -> Delta | str: + """A segment-level update for an outdated translation, or why it is translated as a whole file.""" + suffix = Path(rel).suffix.lower() + if suffix not in (".md", ".json", ".yml", ".yaml"): + return WHOLE_FILE_TYPE + target = target_file(lang, rel) + if not target.exists(): + return "no existing translation" + old = recorded_source_text(rel, entry, run) + if old is None: + return "English version of the translation not found in git" + new, loc = read_text(CONTENT_DIR / rel), read_text(target) + try: + if suffix == ".md": + delta = md_delta(new, old, loc, config.context_segments) + elif suffix == ".json": + delta = json_delta(new, old, loc) + else: + delta = toc_delta(new, old, loc) + except json.JSONDecodeError as e: + return f"unaligned (JSON does not parse: {e})" + if isinstance(delta, str): + return delta + if delta.total_chars and delta.changed_chars > config.max_changed_ratio * delta.total_chars: + return f"mostly changed ({delta.changed_chars * 100 // delta.total_chars}% of the text)" + return delta + + +def map_segment_translations(items: list[dict[str, Any]], data: list[dict[str, Any]]) -> dict[str, str] | str: + """{segment id -> translation} from a /translate/ai response. data[] is positional: either one + entry per content[] item or one per translatable item. An echoed id must match.""" + translatable = [i for i in items if i["translatable"]] + if len(data) == len(items): + pairs = list(zip(items, data, strict=True)) + elif len(data) == len(translatable): + pairs = list(zip(translatable, data, strict=True)) + else: + return f"response has {len(data)} item(s) for {len(items)} segment(s) ({len(translatable)} translatable)" + out: dict[str, str] = {} + for item, d in pairs: + if "id" in d and str(d["id"]) != str(item["id"]): + return f"response item id {d['id']!r} where {item['id']!r} was sent" + if not item["translatable"]: + continue + translation = d.get("translation") + if not isinstance(translation, str): + return f"no translation for segment {item['id']}" + out[str(item["id"])] = translation + return out + + # --------------------------------------------------------------------------- # Translated API client # --------------------------------------------------------------------------- @@ -816,6 +1440,34 @@ def details(self) -> dict[str, Any]: return out +class TranslatedAuthError(TranslatedApiError): + """The API rejected the key. Nothing was billed; the run stops without recording failures.""" + + +def parse_retry_after(value: str | None, now: datetime | None = None) -> float | None: + """Seconds to wait from a Retry-After header (delay-seconds or HTTP-date), or None.""" + if not value or not value.strip(): + return None + v = value.strip() + if v.isdigit(): + return float(v) + try: + when = email.utils.parsedate_to_datetime(v) + except (TypeError, ValueError): + return None + if when.tzinfo is None: + when = when.replace(tzinfo=UTC) + return max(0.0, (when - (now or datetime.now(UTC))).total_seconds()) + + +def retry_delay(attempt: int, retry_after: float | None, jitter: Callable[[], float]) -> float: + """Retry-After when the server sent one, else exponential backoff with jitter; at most MAX_RETRY_DELAY.""" + if retry_after is not None: + return min(retry_after, MAX_RETRY_DELAY) + base = min(float(2**attempt * 2), MAX_RETRY_DELAY) + return base / 2 + jitter() * base / 2 + + def describe_api_error(payload: str) -> tuple[str, dict[str, Any]]: """Build a readable message from an api_error body when it parses, else from the raw text.""" try: @@ -848,10 +1500,12 @@ def __init__( api_key: str, sleep: Callable[[float], None] = time.sleep, opener: Callable[..., Any] = urllib.request.urlopen, + jitter: Callable[[], float] = random.random, ) -> None: self.base_url = base_url self.api_key = api_key self.sleep = sleep + self.jitter = jitter self.opener = opener # urlopen-compatible; injectable so the self-test can fake HTTP answers offline def _request( @@ -871,6 +1525,7 @@ def _request( last_error = "retries exhausted" for attempt in range(retries + 1): req = urllib.request.Request(url, data=data, method=method, headers=headers) + retry_after: float | None = None try: with self.opener(req, timeout=180) as resp: return json.loads(resp.read().decode("utf-8") or "null") @@ -878,14 +1533,22 @@ def _request( payload = e.read().decode("utf-8", errors="replace") message, api_error = describe_api_error(payload) last_error = f"{e.code}: {message}" + if e.code in AUTH_HTTP: + raise TranslatedAuthError( + f"{method} {endpoint} failed with {last_error}. The API key was rejected: check the " + "TRANSLATED_API_KEY secret, and that it belongs to the TRANSLATED_ENV environment.", + e.code, + api_error, + ) from None if e.code not in RETRYABLE_HTTP: raise TranslatedApiError( f"{method} {endpoint} failed with {last_error}", e.code, api_error ) from None + retry_after = parse_retry_after(e.headers.get("Retry-After") if e.headers else None) except (urllib.error.URLError, OSError, http.client.HTTPException, json.JSONDecodeError) as e: last_error = f"{type(e).__name__}: {e}" if attempt < retries: - self.sleep(2**attempt * 2) + self.sleep(retry_delay(attempt, retry_after, self.jitter)) raise TranslatedApiError(f"{method} {endpoint} failed after {retries + 1} attempts ({last_error})") def _post(self, endpoint: str, body: Any, extra_headers: dict[str, str] | None = None, retries: int = 3) -> Any: @@ -987,7 +1650,13 @@ def build_plan( plan.current += 1 continue failure = failures.get(rel) - if failure and failure.get("sourceHash") == src_hash and not options.retry_failed: + # A failed request was not billed, so it is planned again; a rejected delivery waits for --retry-failed. + if ( + failure + and failure.get("sourceHash") == src_hash + and not failure.get("request") + and not options.retry_failed + ): plan.skipped_failed.append((rel, str(failure.get("error")))) continue if not exists or entry.get("status") != STATUS_TRANSLATED: @@ -1043,6 +1712,8 @@ def lang(self, lang: str) -> dict[str, Any]: "removed": [], "chars": 0, # characters submitted "billed_chars": 0, # characters billed by the API + "segmented": [], # (rel, Delta) written from changed segments only + "whole_file": [], # (rel, reason) outdated files that were sent whole }, ) @@ -1065,6 +1736,13 @@ def to_markdown(self, config: TranslationConfig) -> str: for lang, r in sorted(self.per_lang.items()): if r["chars"] or r["billed_chars"]: out.append(f"\n{lang}: {r['chars']:,} characters submitted, {r['billed_chars']:,} billed.") + if r["segmented"]: + changed = sum(d.changed_chars for _, d in r["segmented"]) + total = sum(d.total_chars for _, d in r["segmented"]) + out.append( + f"\n{lang}: {len(r['segmented'])} file(s) updated from changed segments only " + f"({changed:,} of {total:,} translatable characters sent)." + ) if r["repaired"]: kinds = ", ".join(f"{n} {kind}" for kind, n in sorted(r["repaired"].items())) out.append(f"\n{lang}: repaired from English: {kinds} in {len(r['repaired_files'])} file(s).") @@ -1072,6 +1750,10 @@ def to_markdown(self, config: TranslationConfig) -> str: (f"{lang}: failed (translation kept as before)", [f"`{rel}` — {why}" for rel, why in r["failed"]]), (f"{lang}: written with warnings", [f"`{rel}` — {why}" for rel, why in r["warnings"]]), (f"{lang}: removed (English source gone)", [f"`{x}`" for x in r["removed"]]), + ( + f"{lang}: translated as a whole file instead of changed segments", + [f"`{rel}` — {why}" for rel, why in r["whole_file"]], + ), ] for title, items in sections: if items: @@ -1088,7 +1770,10 @@ def to_markdown(self, config: TranslationConfig) -> str: # --------------------------------------------------------------------------- -def print_plan(lang: str, plan: LanguagePlan, limit: int = 0) -> None: +DeltaLookup = Callable[[str, str], Delta | str | None] + + +def print_plan(lang: str, plan: LanguagePlan, limit: int = 0, delta_for: DeltaLookup | None = None) -> None: print( f"\n{lang}: {plan.current} current, {len(plan.to_submit)} to submit, " f"{len(plan.skipped_failed)} failed at this hash, {len(plan.pinned)} pinned, {len(plan.orphans)} orphaned, " @@ -1099,9 +1784,21 @@ def print_plan(lang: str, plan: LanguagePlan, limit: int = 0) -> None: queue = plan.to_submit[:limit] if limit else plan.to_submit chars = 0 for rel, reason in queue: + delta = delta_for(rel, reason) if delta_for else None + if isinstance(delta, Delta) and not delta.changed: + print(f" update {'no text':<12} {rel}") + continue + if isinstance(delta, Delta): + chars += delta.chars + print( + f" submit {'changed':<12} {rel} ({delta.changed}/{delta.total} units, " + f"{delta.changed_chars:,}/{delta.total_chars:,} chars)" + ) + continue content, _, _ = prepare_payload(rel, read_text(CONTENT_DIR / rel)) chars += len(content) - print(f" submit {reason:<12} {rel}") + note = f" (whole file: {delta})" if delta and delta != WHOLE_FILE_TYPE else "" + print(f" submit {reason:<12} {rel}{note}") if len(queue) < len(plan.to_submit): print(f" ... {len(plan.to_submit) - len(queue)} more beyond --limit {limit}") for rel, error in plan.skipped_failed: @@ -1118,9 +1815,29 @@ def print_plan(lang: str, plan: LanguagePlan, limit: int = 0) -> None: print(f" ~{chars:,} characters would be submitted for {lang}") -def cmd_plan(langs: list[str], sources: dict[str, str], passthrough: dict[str, str], options: PlanOptions) -> None: +def file_delta( + config: TranslationConfig, lang: str, rel: str, reason: str, entry: dict[str, Any], options: PlanOptions +) -> Delta | str | None: + """Delta or fallback reason for an outdated file; None for files that are always sent whole.""" + if reason != "outdated" or options.force or not config.segment_diff: + return None + return plan_delta(config, lang, rel, entry) + + +def cmd_plan( + config: TranslationConfig, + langs: list[str], + sources: dict[str, str], + passthrough: dict[str, str], + options: PlanOptions, +) -> None: for lang in langs: - print_plan(lang, build_plan(lang, sources, passthrough, load_status(lang), options), options.limit) + status = load_status(lang) + + def delta_for(rel: str, reason: str, lang: str = lang, status: dict[str, Any] = status) -> Delta | str | None: + return file_delta(config, lang, rel, reason, status["files"].get(rel, {}), options) + + print_plan(lang, build_plan(lang, sources, passthrough, status, options), options.limit, delta_for) def check_existing_translation( @@ -1156,6 +1873,7 @@ def cmd_baseline( run: GitRunner = run_git, ) -> None: ref_hashes: dict[str, str] = {} + sha = "" if ref: sha = resolve_git_ref(ref, run) print(f"baseline ref {ref} -> {sha}") @@ -1203,7 +1921,8 @@ def cmd_baseline( if ref and rel not in ref_hashes: notes.append(f" new {rel}: not in content/ at {ref}; baselined at the current hash") baseline_hash = ref_hashes.get(rel, src_hash) if ref else src_hash - status["files"][rel] = {"sourceHash": baseline_hash, "status": STATUS_TRANSLATED} + ref_blob = blob_at_git_ref(sha, rel, run) if ref and rel in ref_hashes else None + status["files"][rel] = source_entry(rel, baseline_hash, STATUS_TRANSLATED, run, ref_blob) if baseline_hash == src_hash: current += 1 else: @@ -1228,6 +1947,88 @@ def cmd_baseline( print(f" outdated {rel}: {why}") +def cmd_migrate_blobs(langs: list[str], sources: dict[str, str], run: GitRunner = run_git) -> None: + """Record sourceBlob for status entries that only have a sourceHash (entries from before + segment-level updates). The blob is the English version whose content hashes to sourceHash. + Passthrough files are copied, never diffed, and are skipped.""" + if not in_git_work_tree(run): + raise SystemExit("--migrate-blobs needs a git work tree with the history of content/.") + found: dict[tuple[str, str], tuple[str, bytes] | None] = {} + for lang in langs: + status = load_status(lang) + recorded = kept = 0 + missing: list[str] = [] + for rel, entry in status["files"].items(): + source_hash = str(entry.get("sourceHash") or "") + if rel not in sources or not source_hash: + continue + blob = entry.get("sourceBlob") + if isinstance(blob, str) and blob: + data = read_git_blob(blob, run) + if data is not None and hash_bytes(data) == source_hash: + kept += 1 + continue + key = (rel, source_hash) + if key not in found: + found[key] = find_source_blob(rel, source_hash, run) + match = found[key] + if match is None: + missing.append(rel) + continue + rest = {k: v for k, v in entry.items() if k not in ("sourceHash", "sourceBlob")} + status["files"][rel] = {"sourceHash": source_hash, "sourceBlob": match[0], **rest} + recorded += 1 + save_status(lang, status) + print(f"\n{lang}: sourceBlob recorded for {recorded}, already valid for {kept}, not found for {len(missing)}") + for rel in missing: + print(f" not found {rel}: no English version in git hashes to the recorded sourceHash") + + +def status_at_ref(ref: str, lang: str, run: GitRunner = run_git) -> dict[str, Any]: + """The status file of a language at a git ref; empty sections when the file is missing there.""" + proc = run(["git", "show", f"{ref}:{status_path(lang).as_posix()}"]) + status: dict[str, Any] = json.loads(proc.stdout.decode("utf-8")) if proc.returncode == 0 else {} + status.setdefault("files", {}) + status.setdefault("failures", {}) + return status + + +def merge_entries(base: dict[str, Any], ours: dict[str, Any], theirs: dict[str, Any]) -> tuple[dict[str, Any], int]: + """Three-way merge of one status section. An entry the translation branch (theirs) added, changed or + removed since the merge base comes from the branch; every other entry comes from ours, so an entry + the branch merely carries in an older state never overwrites a newer one. Returns (merged, taken).""" + merged: dict[str, Any] = {} + taken = 0 + for key in [*ours, *(k for k in theirs if k not in ours), *(k for k in base if k not in ours and k not in theirs)]: + if theirs.get(key) != base.get(key): + taken += 1 + if key in theirs: + merged[key] = theirs[key] + elif key in ours: + merged[key] = ours[key] + return merged, taken + + +def cmd_merge_status(langs: list[str], ours_ref: str, theirs_ref: str, run: GitRunner = run_git) -> None: + """Rebuild each status file after the translation branch was merged into the run's tree. A text merge + (-X theirs) resolves conflicting hunks of the JSON from the branch; this keeps main's entries the branch + did not touch.""" + if not in_git_work_tree(run): + raise SystemExit("--merge-status needs a git work tree.") + ours_sha, theirs_sha = resolve_git_ref(ours_ref, run), resolve_git_ref(theirs_ref, run) + proc = run(["git", "merge-base", ours_sha, theirs_sha]) + if proc.returncode != 0: + raise SystemExit(f"--merge-status: {ours_ref} and {theirs_ref} have no merge base.") + base_sha = proc.stdout.decode("utf-8").strip() + for lang in langs: + base, ours, theirs = (status_at_ref(sha, lang, run) for sha in (base_sha, ours_sha, theirs_sha)) + status = load_status(lang) + status["files"], taken = merge_entries(base["files"], ours["files"], theirs["files"]) + status["failures"], _ = merge_entries(base["failures"], ours["failures"], theirs["failures"]) + save_status(lang, status) + print(f"{lang}: {taken} status entries taken from {theirs_ref}, the others from {ours_ref}") + + # --------------------------------------------------------------------------- # Commands: run / dump-orders # --------------------------------------------------------------------------- @@ -1242,6 +2043,9 @@ class LanguageSubmission: orders: list[dict[str, Any]] = field(default_factory=list) jobs: dict[str, dict[str, Any]] = field(default_factory=dict) # rel -> pending job record empty_copies: list[str] = field(default_factory=list) # nothing translatable: copied through + deltas: dict[str, Delta] = field(default_factory=dict) # rel -> segment-level update sent in its order + local_deltas: dict[str, Delta] = field(default_factory=dict) # changes without translatable text + whole_file: list[tuple[str, str]] = field(default_factory=list) # outdated files sent whole, with the reason @property def chars(self) -> int: @@ -1249,17 +2053,22 @@ def chars(self) -> int: def build_order( - config: TranslationConfig, id_content: str, content: str, content_type: str, locale: str + config: TranslationConfig, + id_content: str, + content: str, + content_type: str, + locale: str, + segments: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: - # /translate/ai body: one object per file, target_language singular, - # content a list of {text, translatable} segments; the whole file is one segment. + # /translate/ai body: one object per file, target_language singular, content a list of + # {id, text, translatable} segments: the whole file as one segment, or the given segments. order: dict[str, Any] = { "id_content": id_content, "source_language": config.source_locale, "target_language": locale, "content_type": content_type, "service_type": config.service_type, - "content": [{"id": "s1", "text": content, "translatable": True}], + "content": segments if segments is not None else [{"id": "s1", "text": content, "translatable": True}], } if config.instructions: order["context"] = {"instructions": config.instructions} @@ -1280,11 +2089,33 @@ def prepare_submission( sub = LanguageSubmission(lang, locale, status, plan) queue = plan.to_submit[: options.limit] if options.limit else plan.to_submit for rel, reason in queue: + id_content = f"{lang}/{rel}" + failure = status["failures"].get(rel, {}) + if options.retry_failed and failure.get("segmentUpdate") and failure.get("sourceHash") == sources[rel]: + delta: Delta | str | None = "retry of a failed segment update" + else: + delta = file_delta(config, lang, rel, reason, status["files"].get(rel, {}), options) + if isinstance(delta, Delta): + if not delta.changed: + sub.local_deltas[rel] = delta + continue + sub.orders.append(build_order(config, id_content, "", "text/plain", locale, delta.items)) + sub.jobs[rel] = { + "mode": delta.mode, + "idContent": id_content, + "sourceHash": sources[rel], + "reason": "changed", + "contentType": "text/plain", + "chars": delta.chars, + } + sub.deltas[rel] = delta + continue + if delta is not None and delta != WHOLE_FILE_TYPE: + sub.whole_file.append((rel, delta)) content, content_type, info = prepare_payload(rel, read_text(CONTENT_DIR / rel)) if not content.strip() or info.get("units") == 0: sub.empty_copies.append(rel) continue - id_content = f"{lang}/{rel}" sub.orders.append(build_order(config, id_content, content, content_type, locale)) sub.jobs[rel] = { **info, @@ -1334,10 +2165,10 @@ def apply_local_changes( r["copied"].append(rel) print(f"{lang}: copied passthrough {rel}") # Always (re)record the entry so legacy "translated" tags on passthrough files become "copied". - status["files"][rel] = {"sourceHash": passthrough[rel], "status": STATUS_COPIED} + status["files"][rel] = source_entry(rel, passthrough[rel], STATUS_COPIED) for rel in sub.empty_copies: write_text(target_file(lang, rel), read_text(CONTENT_DIR / rel)) - status["files"][rel] = {"sourceHash": sources[rel], "status": STATUS_TRANSLATED} + status["files"][rel] = source_entry(rel, sources[rel], STATUS_TRANSLATED) status["failures"].pop(rel, None) save_status(lang, status) @@ -1346,6 +2177,36 @@ def billed_chars(data: list[dict[str, Any]]) -> int: return sum(int(d["billed_chars"]) for d in data if isinstance(d.get("billed_chars"), (int, float))) +def write_delivery( + lang: str, + rel: str, + source_hash: str, + text: str, + problems: list[str], + warnings: list[str], + status: dict[str, Any], + lenient: bool, + structured: bool, +) -> list[str]: + """Write a verified delivery and record it. Returns the problems that rejected it (empty when written). + --lenient only makes sense for raw Markdown/HTML, where the assembled text is still usable. + A structured payload (toc names, UI strings) with problems rebuilds to English or nothing.""" + if problems and (not lenient or not text or structured): + if lenient: + why = "no usable text" if not text else "structured payload cannot be written partially" + print(f"{lang}: --lenient ignored for {rel}: {why}") + record_failure(status, rel, "; ".join(problems), source_hash) + return problems + write_text(target_file(lang, rel), text) + status["files"][rel] = source_entry(rel, source_hash, STATUS_TRANSLATED) + status["failures"].pop(rel, None) + if problems: + print(f"{lang}: written despite problems ({'; '.join(problems)}): {rel}") + if warnings: + print(f"{lang}: written with warnings ({'; '.join(warnings)}): {rel}") + return [] + + def apply_translation( lang: str, rel: str, @@ -1360,28 +2221,65 @@ def apply_translation( if not isinstance(translated, str) or not translated: record_failure(status, rel, "no translation in the response", job_hash) return ["no translation in the response"], Counter(), [] + spans = job.get("codeSpans") + if isinstance(spans, list): + translated, mask_problems = unmask_code(translated, spans) + if mask_problems: + record_failure(status, rel, "; ".join(mask_problems), job_hash) + return mask_problems, Counter(), [] try: text, problems, repaired, warnings = finalize_translation(rel, job, read_text(CONTENT_DIR / rel), translated) except Exception as e: # noqa: BLE001 - reported to the reviewer, never crashes the run record_failure(status, rel, f"could not reinsert translation: {e}", job_hash) return [f"could not reinsert translation: {e}"], Counter(), [] - # --lenient only makes sense for raw Markdown/HTML, where the assembled text is still usable. - # A structured payload (toc names, UI strings) with problems rebuilds to English or nothing. structured = job.get("mode") in ("yaml-names", "json-placeholders") - if problems and (not lenient or not text or structured): - if lenient: - why = "no usable text" if not text else "structured payload cannot be written partially" - print(f"{lang}: --lenient ignored for {rel}: {why}") - record_failure(status, rel, "; ".join(problems), job_hash) - return problems, repaired, warnings - write_text(target_file(lang, rel), text) - status["files"][rel] = {"sourceHash": job_hash, "status": STATUS_TRANSLATED} - status["failures"].pop(rel, None) + rejected = write_delivery(lang, rel, job_hash, text, problems, warnings, status, lenient, structured) + return rejected, repaired, warnings + + +def apply_delta( + lang: str, + rel: str, + source_hash: str, + delta: Delta, + translations: dict[str, str], + status: dict[str, Any], + lenient: bool, +) -> tuple[list[str], Counter[str], list[str]]: + """Rebuild one file from reused, translated and English blocks, verify and write it.""" + try: + text, problems, repaired, warnings = delta.assemble(translations) + except Exception as e: # noqa: BLE001 - reported to the reviewer, never crashes the run + record_failure(status, rel, f"could not reassemble translation: {e}", source_hash, segmentUpdate=True) + return [f"could not reassemble translation: {e}"], Counter(), [] + structured = delta.mode != "md-blocks" + rejected = write_delivery(lang, rel, source_hash, text, problems, warnings, status, lenient, structured) + if rejected: + status["failures"][rel]["segmentUpdate"] = True + return rejected, repaired, warnings + + +def record_result( + lang: str, + rel: str, + r: dict[str, Any], + problems: list[str], + repaired: Counter[str], + warnings: list[str], + done: str, +) -> None: if problems: - print(f"{lang}: written despite problems ({'; '.join(problems)}): {rel}") + r["failed"].append((rel, "; ".join(problems))) + print(f"{lang}: REJECTED {rel}: {'; '.join(problems)}") + return + r["translated"].append(rel) + if repaired: + r["repaired"].update(repaired) + r["repaired_files"].append(rel) if warnings: - print(f"{lang}: written with warnings ({'; '.join(warnings)}): {rel}") - return [], repaired, warnings + r["warnings"].append((rel, "; ".join(warnings))) + suffix = f" (repaired {dict(repaired)})" if repaired else "" + print(f"{lang}: {done}{suffix}") def translate_language( @@ -1391,45 +2289,80 @@ def translate_language( passthrough: dict[str, str], lenient: bool, report: RunReport, + rejected_dir: Path | None = None, ) -> None: """One synchronous /translate/ai request per planned file: translate, repair, verify, write. - The status file is saved after every file so an interrupted run loses no billed work.""" + The status file is saved after every file so an interrupted run loses no billed work. A rejected + key stops the run (TranslatedAuthError) before anything else is recorded for the file.""" lang, status = sub.lang, sub.status r = report.lang(lang) apply_local_changes(sub, sources, passthrough, r) + r["whole_file"].extend(sub.whole_file) orders = {str(o["id_content"]): o for o in sub.orders} try: + for rel, local in sub.local_deltas.items(): + problems, repaired, warnings = apply_delta(lang, rel, sources[rel], local, {}, status, lenient) + if not problems: + r["segmented"].append((rel, local)) + record_result(lang, rel, r, problems, repaired, warnings, f"updated {rel} (no changed text to translate)") + save_status(lang, status) for rel, job in sub.jobs.items(): + order = orders[job["idContent"]] try: - data = client.translate_ai(orders[job["idContent"]]) + data = client.translate_ai(order) + except TranslatedAuthError: + raise except TranslatedApiError as e: report.request_failures += 1 - record_failure(status, rel, f"POST translate/ai failed: {e}", job["sourceHash"], apiError=e.details()) + record_failure( + status, rel, f"POST translate/ai failed: {e}", job["sourceHash"], apiError=e.details(), request=True + ) r["failed"].append((rel, f"POST translate/ai failed: {e}")) print(f"{lang}: FAILED {rel}: {e}") save_status(lang, status) continue r["chars"] += int(job["chars"]) r["billed_chars"] += billed_chars(data) - translated = data[0].get("translation") if data else None - problems, repaired, warnings = apply_translation(lang, rel, job, translated, status, lenient) - if problems: - r["failed"].append((rel, "; ".join(problems))) - print(f"{lang}: REJECTED {rel}: {'; '.join(problems)}") + delta = sub.deltas.get(rel) + if delta is None: + translated = data[0].get("translation") if data else None + problems, repaired, warnings = apply_translation(lang, rel, job, translated, status, lenient) + done = f"translated {rel} ({job['reason']}, {job['chars']:,} chars)" else: - r["translated"].append(rel) - if repaired: - r["repaired"].update(repaired) - r["repaired_files"].append(rel) - if warnings: - r["warnings"].append((rel, "; ".join(warnings))) - suffix = f" (repaired {dict(repaired)})" if repaired else "" - print(f"{lang}: translated {rel} ({job['reason']}, {job['chars']:,} chars){suffix}") + translations = map_segment_translations(order["content"], data) + if isinstance(translations, str): + record_failure(status, rel, translations, job["sourceHash"], segmentUpdate=True) + problems, repaired, warnings = [translations], Counter(), [] + else: + problems, repaired, warnings = apply_delta( + lang, rel, job["sourceHash"], delta, translations, status, lenient + ) + if not problems: + r["segmented"].append((rel, delta)) + done = ( + f"translated {rel} (changed segments {delta.changed}/{delta.total}, " + f"{delta.changed_chars:,}/{delta.total_chars:,} chars)" + ) + record_result(lang, rel, r, problems, repaired, warnings, done) + if problems and rejected_dir is not None: + kept = keep_rejected(rejected_dir, lang, rel, order, data, problems) + print(f"{lang}: kept the rejected delivery of {rel} in {kept}") save_status(lang, status) finally: save_status(lang, status) +def keep_rejected( + out_dir: Path, lang: str, rel: str, order: dict[str, Any], data: list[dict[str, Any]], problems: list[str] +) -> Path: + """Save a billed delivery that failed verification, so it can be repaired by hand instead of bought again.""" + path = out_dir / lang / f"{rel}.json" + path.parent.mkdir(parents=True, exist_ok=True) + record = {"language": lang, "file": rel, "problems": problems, "order": order, "response": data} + path.write_text(json.dumps(record, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + return path + + def prepare_all( config: TranslationConfig, langs: list[str], @@ -1456,9 +2389,10 @@ def cmd_run( allow_unbounded: bool, lenient: bool, report: RunReport, + rejected_dir: Path | None = None, ) -> None: for sub in prepare_all(config, langs, sources, passthrough, locales, options, allow_unbounded): - translate_language(client, sub, sources, passthrough, lenient, report) + translate_language(client, sub, sources, passthrough, lenient, report, rejected_dir) def cmd_dump_orders( @@ -1489,6 +2423,12 @@ def cmd_dump_orders( "characters": sub.chars, "passthroughCopies": sub.plan.to_copy, "emptyCopies": sub.empty_copies, + "segmented": { + rel: {"units": f"{d.changed}/{d.total}", "chars": f"{d.changed_chars}/{d.total_chars}"} + for rel, d in sub.deltas.items() + }, + "localUpdates": sorted(sub.local_deltas), + "wholeFile": dict(sub.whole_file), "orphans": sub.plan.orphans, "unscoped": sub.plan.unscoped, "pinned": sub.plan.pinned, @@ -1558,6 +2498,14 @@ def check(self, name: str, ok: bool, detail: str = "") -> None: self.failures.append(name) print(f" FAIL {name}" + (f" -- {detail[:300]}" if detail else "")) + def expect_raises(self, name: str, fn: Callable[[], object], error: type[BaseException]) -> None: + try: + fn() + except error: + self.check(name, True) + return + self.check(name, False, f"no {error.__name__} raised") + def expect_exit(self, name: str, fn: Callable[[], object], fragment: str = "") -> None: try: fn() @@ -1608,7 +2556,11 @@ def selftest_corpus(t: SelfTest, config: TranslationConfig) -> None: text = read_text(CONTENT_DIR / rel) content, _, info = prepare_payload(rel, text) try: - out, problems, repaired, warnings = finalize_translation(rel, info, text, content, check_identity=False) + delivered, mask_problems = unmask_code(content, info.get("codeSpans", [])) + if mask_problems: + bad.append(f"{rel}: {mask_problems}") + continue + out, problems, repaired, warnings = finalize_translation(rel, info, text, delivered, check_identity=False) except Exception as e: # noqa: BLE001 bad.append(f"{rel}: raised {e}") continue @@ -1618,6 +2570,35 @@ def selftest_corpus(t: SelfTest, config: TranslationConfig) -> None: passthrough = get_passthrough_sources(config, sources) t.check("passthrough files are outside the translation scope", not set(passthrough) & set(sources)) + t.section("corpus segmentation (segment -> join, unchanged page rebuilds to itself)") + md_sources = [rel for rel in sources if rel.endswith(".md")] + bad = [] + for rel in md_sources: + text = read_text(CONTENT_DIR / rel) + fm, body = split_frontmatter(to_lf(text.lstrip(BOM))) + seg = segment(body) + if seg.join() != body: + bad.append(f"{rel}: segment/join differs") + continue + # Stand-in translation: " (es)" after the first line of every translatable block. + marked = [b.text.replace("\n", " (es)\n", 1) if b.translatable else b.text for b in seg.blocks] + translation = fm + seg.join(marked) + delta = md_delta(text, text, translation) + if isinstance(delta, str) or delta.changed: + bad.append(f"{rel}: {delta if isinstance(delta, str) else f'{delta.changed} changed unit(s)'}") + continue + out, problems, _, _ = delta.assemble({}) + expected = (BOM if text.startswith(BOM) else "") + translation + if "\r\n" in text: + expected = expected.replace("\n", "\r\n") + if out != expected or problems: + bad.append(f"{rel}: {'differs' if out != expected else ''} {problems}") + t.check( + f"{len(md_sources)} Markdown sources segment, and an unchanged page rebuilds its translation byte-for-byte", + not bad, + "; ".join(bad[:5]), + ) + def selftest_fixtures(t: SelfTest) -> None: t.section("markdown repair-then-verify") @@ -1649,11 +2630,8 @@ def selftest_fixtures(t: SelfTest) -> None: dropped_span = identity.replace("`Selected.Tables`", "Selected.Tables") out, problems, repaired, warnings = fin(FIXTURE_MD, dropped_span) t.check( - "dropped inline code span -> warning, not a problem; delivered text kept", - not problems - and not repaired - and warnings == ["inline code spans 2 -> 1 (missing ['`Selected.Tables`'], added [])"] - and out == BOM + dropped_span, + "dropped inline code span -> problem (the code was lost)", + problems == ["inline code spans 2 -> 1 (missing ['`Selected.Tables`'], added [])"] and not warnings, f"{problems} {warnings}", ) extra_span = identity.replace("Open `View", "Open `true` `View") @@ -1665,16 +2643,20 @@ def selftest_fixtures(t: SelfTest) -> None: and out == BOM + extra_span, f"{problems} {warnings}", ) - out, problems, repaired, warnings = fin(FIXTURE_MD, dropped_span.replace("(guide.md)", "(guia.md)")) + out, problems, repaired, warnings = fin(FIXTURE_MD, extra_span.replace("(guide.md)", "(guia.md)")) t.check( - "inline-code drift does not block the other repairs (link target still restored)", - not problems and len(warnings) == 1 and repaired["links"] == 1 and out == BOM + dropped_span, + "an added inline code span does not block the other repairs (link target still restored)", + not problems and len(warnings) == 1 and repaired["links"] == 1 and out == BOM + extra_span, f"{problems} {warnings} {dict(repaired)}", ) - _, problems, _, warnings = fin(FIXTURE_MD, dropped_span, repair=False) + _, problems, _, warnings = fin(FIXTURE_MD, extra_span, repair=False) t.check( - "inline-code drift is a warning in verify-only mode too", not problems and len(warnings) == 1, str(problems) + "an added inline code span is a warning in verify-only mode too", + not problems and len(warnings) == 1, + str(problems), ) + _, problems, _, warnings = fin(FIXTURE_MD, dropped_span, repair=False) + t.check("a dropped inline code span is a problem in verify-only mode too", len(problems) == 1, str(problems)) out, problems, repaired, _ = fin(FIXTURE_MD, identity.replace("[!NOTE]", "[!NOTA]")) t.check( "[!NOTE] -> [!NOTA] (equal count) -> repaired", out == FIXTURE_MD and not problems and repaired["markers"] == 1 @@ -1953,15 +2935,19 @@ def __init__(self) -> None: self.orders: list[dict[str, Any]] = [] self.translate_calls = 0 self.raise_on_translate_call: int | None = None # 1-based call index that raises an API error - self.deliveries: dict[str, str] = {} # id_content -> translated text (default: echo the source) + self.raise_http_status = 400 # status of that error; 401/403 raise TranslatedAuthError + # id_content -> translated text of a whole-file order, or {segment id -> text} (default: echo the source) + self.deliveries: dict[str, str | dict[str, str]] = {} + self.translatable_only = False # answer with data[] items for translatable segments only self.empty_responses: set[str] = set() # id_content answered with an empty data[] def translate_ai(self, order: dict[str, Any]) -> list[dict[str, Any]]: self.translate_calls += 1 if self.raise_on_translate_call == self.translate_calls: - raise TranslatedApiError( - "POST translate/ai failed with 400: INVALID_PARAMS; boom", - 400, + error = TranslatedAuthError if self.raise_http_status in AUTH_HTTP else TranslatedApiError + raise error( + f"POST translate/ai failed with {self.raise_http_status}: INVALID_PARAMS; boom", + self.raise_http_status, { "code": "INVALID_PARAMS", "message": "boom", @@ -1973,8 +2959,23 @@ def translate_ai(self, order: dict[str, Any]) -> list[dict[str, Any]]: id_content = str(order["id_content"]) if id_content in self.empty_responses: return [] - text = self.deliveries.get(id_content, str(order["content"][0]["text"])) - return [{"translation": text, "translatable": True, "billed_chars": len(text)}] + delivery = self.deliveries.get(id_content) + items: list[dict[str, Any]] = order["content"] + out: list[dict[str, Any]] = [] + for item in items: + if isinstance(delivery, dict): + text = delivery.get(str(item["id"]), str(item["text"])) + elif delivery is not None and len(items) == 1: + text = delivery + else: + text = str(item["text"]) + if self.translatable_only and not item["translatable"]: + continue + billed = len(text) if item["translatable"] else 0 + out.append( + {"id": item["id"], "translation": text, "translatable": item["translatable"], "billed_chars": billed} + ) + return out def service_type_names(self) -> list[dict[str, Any]]: return [ @@ -2030,14 +3031,48 @@ def mini_repo() -> Iterator[Path]: for rel, text in MINI_FILES.items(): write_text(tmp / CONTENT_DIR / rel, text) (tmp / LOCALIZED_DIR / "es" / "content").mkdir(parents=True) + ceiling = os.environ.get("GIT_CEILING_DIRECTORIES") + # Keep git from finding a repository above the temp dir; git_mini_repo creates one inside it. + os.environ["GIT_CEILING_DIRECTORIES"] = str(tmp.parent) os.chdir(tmp) try: yield tmp finally: os.chdir(cwd) + if ceiling is None: + os.environ.pop("GIT_CEILING_DIRECTORIES", None) + else: + os.environ["GIT_CEILING_DIRECTORIES"] = ceiling + for p in tmp.rglob("*"): + os.chmod(p, 0o700) # git object files are read-only; rmtree cannot delete them on Windows shutil.rmtree(tmp, ignore_errors=True) +def git(*args: str) -> str: + return subprocess.run(["git", *args], check=True, capture_output=True).stdout.decode("utf-8").strip() + + +def commit_all(message: str) -> None: + git("add", "-A") + git("commit", "-q", "--allow-empty", "-m", message) + + +@contextmanager +def git_mini_repo(extra: dict[str, str] | None = None) -> Iterator[Path]: + """mini_repo as a git repository with the English sources (plus `extra`) committed.""" + with mini_repo() as tmp: + for rel, text in (extra or {}).items(): + write_text(CONTENT_DIR / rel, text) + git("init", "-q") + git("config", "user.email", "selftest@example.com") + git("config", "user.name", "selftest") + git("config", "core.autocrlf", "false") + git("config", "commit.gpgsign", "false") + git("config", "core.hooksPath", ".git/no-hooks") + commit_all("English sources") + yield tmp + + @dataclass class Harness: config: TranslationConfig @@ -2053,7 +3088,13 @@ def sources(self) -> dict[str, str]: def passthrough(self) -> dict[str, str]: return get_passthrough_sources(self.config, self.sources) - def run(self, options: PlanOptions | None = None, allow_unbounded: bool = True, lenient: bool = False) -> None: + def run( + self, + options: PlanOptions | None = None, + allow_unbounded: bool = True, + lenient: bool = False, + rejected_dir: Path | None = None, + ) -> None: cmd_run( self.config, self.client, @@ -2065,6 +3106,7 @@ def run(self, options: PlanOptions | None = None, allow_unbounded: bool = True, allow_unbounded, lenient, self.report, + rejected_dir, ) def plan(self, options: PlanOptions | None = None) -> LanguagePlan: @@ -2166,6 +3208,22 @@ def selftest_fake_client(t: SelfTest) -> None: ) t.check("file not written", not target_file("es", "guide.md").exists()) t.check("other files translated", status["files"]["index.md"]["status"] == STATUS_TRANSLATED) + kept_dir = Path("rejected") + h.run(PlanOptions(retry_failed=True, file_filters=["guide.md"]), rejected_dir=kept_dir) + kept_path = kept_dir / "es" / "guide.md.json" + kept = json.loads(kept_path.read_text(encoding="utf-8")) if kept_path.exists() else {} + t.check( + "--keep-rejected saves the billed delivery with its order and problems", + kept.get("file") == "guide.md" + and "headings" in kept["problems"][0] + and kept["order"]["id_content"] == "es/guide.md" + and kept["response"][0]["translation"] == h.client.deliveries["es/guide.md"], + str(kept)[:300], + ) + t.check( + "only rejected deliveries are kept", + [p.name for p in kept_dir.rglob("*.json")] == ["guide.md.json"], + ) t.check("verification failures do not flag the run (exit 0)", not h.report.failed) plan = h.plan() t.check( @@ -2300,6 +3358,31 @@ def selftest_fake_client(t: SelfTest) -> None: str(status["failures"]), ) t.check("run flagged as failed (exit 1)", h.report.request_failures == 1 and h.report.failed) + t.check( + "a failed request is not billed, so the next plan sends the file again without --retry-failed", + [rel for rel, _ in h.plan().to_submit] == failed and not h.plan().skipped_failed, + ) + h.client.raise_on_translate_call = None + h.run() + status = load_status("es") + t.check( + "the next run translates it and clears the failure", + not status["failures"] and status["files"][failed[0]]["status"] == STATUS_TRANSLATED, + str(status["failures"]), + ) + with mini_repo(): + h = prod_harness() + h.client.raise_on_translate_call = 2 + h.client.raise_http_status = 401 + t.expect_raises("a rejected key (401) stops the run", lambda: h.run(), TranslatedAuthError) + status = load_status("es") + t.check( + "after a rejected key: the file translated before is saved, nothing is recorded as failed", + sum(1 for f in status["files"].values() if f["status"] == STATUS_TRANSLATED) == 1 + and not status["failures"] + and len(h.plan().to_submit) == 3, + str(status), + ) t.section("fake client: sandbox caps") with mini_repo(): @@ -2525,11 +3608,853 @@ def fake_git(cmd: list[str]) -> subprocess.CompletedProcess[bytes]: ) +# --- segment-level updates ------------------------------------------------------ + +SEG_MD = ( + "---\ntitle: Refresh guide\ndescription: How to refresh\nuid: refresh\n---\n" + "# Refresh guide\n\nOpen the dialog with **Ctrl+R**.\n\n" + "- First option explained.\n- Second option explained.\n\n" + "| Option | Meaning |\n|---|---|\n| Full | Refresh everything |\n\n" + "```csharp\nvar x = 1;\n```\n\n" + "\n\n- @refresh-uid\n\n" + "## Details\n\nThis paragraph is removed later.\n\nFinal paragraph stays.\n" +) +SEG_MD_ES = ( + "---\ntitle: Guía de actualización\ndescription: Cómo actualizar\nuid: refresh\n---\n" + "# Guía de actualización\n\nAbre el cuadro de diálogo con **Ctrl+R**.\n\n" + "- Primera opción explicada.\n- Segunda opción explicada.\n\n" + "| Opción | Significado |\n|---|---|\n| Full | Actualiza todo |\n\n" + "```csharp\nvar x = 1;\n```\n\n" + "\n\n- @refresh-uid\n\n" + "## Detalles\n\nEste párrafo se elimina más tarde.\n\nEl párrafo final se mantiene.\n" +) +SEG_MD_V2 = ( + SEG_MD.replace("title: Refresh guide", "title: Refresh guide for models") + .replace("**Ctrl+R**", "**Ctrl+Shift+R**") + .replace("- Second option explained.\n", "- Second option explained.\n- Third option explained.\n") + .replace("var x = 1;", "var x = 2;") + .replace("This paragraph is removed later.\n\n", "") +) +SEG_MD_ES_V2 = ( + SEG_MD_ES.replace("title: Guía de actualización", "title: Guía de actualización de modelos") + .replace("**Ctrl+R**", "**Ctrl+Shift+R**") + .replace("- Segunda opción explicada.\n", "- Segunda opción explicada.\n- Tercera opción explicada.\n") + .replace("var x = 1;", "var x = 2;") + .replace("Este párrafo se elimina más tarde.\n\n", "") +) +SEG_V2_DELIVERY = { + "fm:title": "Guía de actualización de modelos", + "b1": "Abre el cuadro de diálogo con **Ctrl+Shift+R**.", + "b4": "- Tercera opción explicada.", +} +SEGMENT_SAMPLE = ( + "\n# Title\n\nPara one\ncontinues here.\n\n- item one\n continued\n- item two\n***\n\n" + "> [!NOTE]\n> Note line\n> second line\n\n| a | b |\n|---|---|\n\n" + "```\ncode\n\nmore code\n```\n\n\n\n" + "[!include[part](includes/part.md)]\n\n- @uid-one, @uid-two\n\n" + 'Dialog picture\n\nhttps://example.com\n' +) + + +def selftest_segments(t: SelfTest) -> None: + t.section("block segmentation and block diff") + seg = segment(SEGMENT_SAMPLE) + t.check("segment + join round-trips byte-for-byte", seg.join() == SEGMENT_SAMPLE) + t.check( + "kinds: heading, paragraph, list items (with continuation), rule, alert, table rows, fence, comment", + seg.kinds == ["h", "p", "li", "li", "hr", "alert", "tr", "tr", "fence", "comment", "p", "li", "p", "p"], + str(seg.kinds), + ) + t.check( + "untranslatable: rule, table separator, fence, comment, include line, xref-only item, bare URL", + [b.translatable for b in seg.blocks] + == [True, True, True, True, False, True, True, False, False, False, False, False, True, False], + str([b.translatable for b in seg.blocks]), + ) + t.check("leading blank line kept as the lead", seg.lead == "\n" and seg.blocks[0].text == "# Title\n") + a, b, c = (Block("p", f"{x}\n", "\n") for x in "ABC") + moved = diff_blocks([a, b, c], [c, a, b]) + t.check( + "a moved block reuses its old translation", + [(m.kind, m.old_index) for m in moved] == [(ACTION_REUSE, 2), (ACTION_REUSE, 0), (ACTION_REUSE, 1)], + str(moved), + ) + d = Block("p", "D\n", "\n") + actions = diff_blocks([a, b, c], [a, d, c]) + t.check( + "a changed block is translated, its neighbours reused", + [m.kind for m in actions] == [ACTION_REUSE, ACTION_TRANSLATE, ACTION_REUSE], + ) + t.check("context picks one unchanged block on each side", context_indices(actions, [a, d, c], 1) == [0, 2]) + link_old, link_new = Block("p", "See [x](a.md) and `A`.\n"), Block("p", "See [x](b.md) and `B`.\n") + t.check( + "a block whose only change is code or a link target is reused with repair", + [(m.kind, m.old_index) for m in diff_blocks([link_old], [link_new], prose_key)] == [(ACTION_REPAIR, 0)], + ) + t.check( + "without prose_key the same change is translated", + [m.kind for m in diff_blocks([link_old], [link_new])] == [ACTION_TRANSLATE], + ) + t.check( + "a heading whose only change is its level is reused with repair; changed heading text is translated", + [m.kind for m in diff_blocks([Block("h", "##### Setup\n")], [Block("h", "## Setup\n")], prose_key)] + == [ACTION_REPAIR] + and [m.kind for m in diff_blocks([Block("h", "## Setup\n")], [Block("h", "## Set up\n")], prose_key)] + == [ACTION_TRANSLATE], + ) + t.check( + "heading marks restored from English", + restore_heading_marks("## Setup\n", "##### Configuración\n") == "## Configuración\n" + and restore_heading_marks("## Setup\n", "## Configuración\n") == "## Configuración\n", + ) + t.check("width 0 sends no context", context_indices(actions, [a, d, c], 0) == []) + t.check( + "width -1 sends every unchanged translatable block", + context_indices([*actions, Action(ACTION_ENGLISH)], [a, d, c, Block("fence", "```\nx\n```\n")], -1) == [0, 2], + ) + t.check("no context without a changed block", context_indices([Action(ACTION_REUSE, 0)], [a], -1) == []) + t.check( + "context width defaults to 2; TRANSLATED_CONTEXT_SEGMENTS overrides the config value", + TranslationConfig(mini_build_config()).context_segments == 2 + and TranslationConfig(mini_build_config(), None, None, "5").context_segments == 5 + and TranslationConfig(mini_build_config(), None, None, " ").context_segments == 2, + ) + t.expect_exit( + "a context width that is not an integer stops the run", + lambda: TranslationConfig(mini_build_config(), None, None, "many"), + "Context segments must be an integer", + ) + fence_old, fence_new = Block("fence", "```\nx\n```\n"), Block("fence", "```\ny\n```\n") + t.check( + "a changed untranslatable block is taken from the English", + [m.kind for m in diff_blocks([fence_old], [fence_new])] == [ACTION_ENGLISH], + ) + text, problems, repaired, _ = finalize_block(Block("li", "- Item one\n"), "* Elemento uno") + t.check( + "delivered list marker restored from English", + text == "- Elemento uno\n" and not problems and repaired["markers"] == 1, + f"{text!r} {problems}", + ) + _, problems, _, _ = finalize_block(Block("h", "## Details\n"), "### Detalles") + t.check("heading level change is a problem", problems == ["heading level ## -> ###"], str(problems)) + _, problems, _, _ = finalize_block(Block("p", "One line.\n"), "Línea uno.\n\nLínea dos.") + t.check("a block split in two is a problem", problems == ["p block came back as p + p"], str(problems)) + _, problems, _, _ = finalize_block(Block("h", "## Details\n"), "Detalles") + t.check("a heading delivered as text is a problem", problems == ["h block came back as p"], str(problems)) + reordered = restore_positionally("usa `b` y `a`", "use `a` and `b`", INLINE_CODE_RE, 0) + t.check("reordered inline code is left as delivered", reordered == ("usa `b` y `a`", 0), str(reordered)) + items = [ + {"id": "c0", "text": "x", "translatable": False}, + {"id": "b1", "text": "y", "translatable": True}, + ] + t.check( + "response mapped with one item per segment", + map_segment_translations(items, [{"translation": "x"}, {"translation": "Y"}]) == {"b1": "Y"}, + ) + t.check( + "response mapped with one item per translatable segment", + map_segment_translations(items, [{"id": "b1", "translation": "Y"}]) == {"b1": "Y"}, + ) + t.check( + "response with another item count is an error", + isinstance(map_segment_translations(items, [{}, {}, {}]), str), + ) + t.check( + "response with a mismatched id is an error", + "id 'b9'" in str(map_segment_translations(items, [{"id": "c0"}, {"id": "b9", "translation": "Y"}])), + ) + + +def seg_harness(context: int = 0, ratio: float = 0.8) -> Harness: + cfg = mini_build_config() + cfg["translation"]["segmentDiff"] = {"enabled": True, "maxChangedRatio": ratio, "contextSegments": context} + return Harness(TranslationConfig(cfg, "production", "mt-test")) + + +def seed_seg_translation(h: Harness) -> None: + """Whole-file translation of seg.md at the committed English, then commit it.""" + h.client.deliveries["es/seg.md"] = mask_code(SEG_MD_ES)[0] + h.run(PlanOptions(file_filters=["seg.md"])) + commit_all("Spanish") + + +def selftest_segment_updates(t: SelfTest) -> None: + t.section("segment-level updates: only changed blocks are sent") + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness() + seed_seg_translation(h) + entry = load_status("es")["files"]["seg.md"] + t.check( + "whole-file run records sourceBlob of the English", + entry.get("sourceBlob") == git("hash-object", "content/seg.md"), + str(entry), + ) + write_text(CONTENT_DIR / "seg.md", SEG_MD_V2) + commit_all("English v2") + out = captured( + lambda: print_plan( + "es", + h.plan(), + 0, + lambda rel, reason: file_delta( + h.config, "es", rel, reason, load_status("es")["files"].get(rel, {}), PlanOptions() + ), + ) + ) + t.check("--plan shows the changed units", "submit changed seg.md (3/11 units" in out, out) + h.client.deliveries["es/seg.md"] = SEG_V2_DELIVERY + h.report = RunReport() + h.run(PlanOptions(file_filters=["seg.md"])) + order = h.client.orders[-1] + t.check( + "order holds only the changed title, paragraph and list item", + [c["id"] for c in order["content"]] == ["fm:title", "b1", "b4"] + and all(c["translatable"] for c in order["content"]) + and order["content_type"] == "text/plain", + str(order["content"]), + ) + written = read_text(target_file("es", "seg.md")) + t.check( + "file rebuilt: unchanged blocks kept, changed blocks translated, code from English, removed block gone", + written == SEG_MD_ES_V2, + written, + ) + entry = load_status("es")["files"]["seg.md"] + t.check( + "status advanced to the new English hash and blob", + entry + == { + "sourceHash": h.sources["seg.md"], + "sourceBlob": git("hash-object", "content/seg.md"), + "status": STATUS_TRANSLATED, + }, + str(entry), + ) + r = h.report.lang("es") + md = h.report.to_markdown(h.config) + t.check( + "report counts the file as updated from changed segments", + [rel for rel, _ in r["segmented"]] == ["seg.md"] + and r["chars"] == sum(len(c["text"]) for c in order["content"]) + and "1 file(s) updated from changed segments only" in md, + md, + ) + commit_all("Spanish v2") + + write_text(CONTENT_DIR / "seg.md", SEG_MD_V2.replace("var x = 2;", "var x = 3;")) + commit_all("code only") + sent = len(h.client.orders) + out = captured(lambda: h.run(PlanOptions(file_filters=["seg.md"]))) + t.check( + "a code-only change is applied without a request", + len(h.client.orders) == sent + and read_text(target_file("es", "seg.md")) == SEG_MD_ES_V2.replace("var x = 2;", "var x = 3;") + and "updated seg.md (no changed text to translate)" in out + and load_status("es")["files"]["seg.md"]["sourceHash"] == h.sources["seg.md"], + out, + ) + commit_all("Spanish code only") + + write_text(CONTENT_DIR / "seg.md", read_text(CONTENT_DIR / "seg.md").replace("## Details\n", "### Details\n")) + commit_all("heading level only") + sent = len(h.client.orders) + expected = read_text(target_file("es", "seg.md")).replace("## Detalles\n", "### Detalles\n") + out = captured(lambda: h.run(PlanOptions(file_filters=["seg.md"]))) + t.check( + "a heading-level change is applied without a request", + len(h.client.orders) == sent + and read_text(target_file("es", "seg.md")) == expected + and "updated seg.md (no changed text to translate)" in out + and load_status("es")["files"]["seg.md"]["sourceHash"] == h.sources["seg.md"], + out, + ) + commit_all("Spanish heading level") + + before = read_text(target_file("es", "seg.md")) + status_before = load_status("es")["files"]["seg.md"] + write_text(CONTENT_DIR / "seg.md", read_text(CONTENT_DIR / "seg.md").replace("stays.", "stays here.")) + h.client.deliveries["es/seg.md"] = {"b12": "Línea uno.\n\nLínea dos."} + h.run(PlanOptions(file_filters=["seg.md"])) + status = load_status("es") + t.check( + "a delivery that splits a block is rejected; file and status kept", + "p block came back as p + p" in status["failures"].get("seg.md", {}).get("error", "") + and read_text(target_file("es", "seg.md")) == before + and status["files"]["seg.md"] == status_before, + str(status["failures"]), + ) + h.client.deliveries["es/seg.md"] = mask_code(before.replace("se mantiene.", "se queda aquí."))[0] + h.run(PlanOptions(file_filters=["seg.md"], retry_failed=True)) + status = load_status("es") + t.check( + "--retry-failed after a failed segment update sends the whole file; written, failure cleared", + [c["id"] for c in h.client.orders[-1]["content"]] == ["s1"] + and "seg.md" not in status["failures"] + and read_text(target_file("es", "seg.md")).endswith("El párrafo final se queda aquí.\n"), + str(status["failures"]), + ) + commit_all("final paragraph") + + write_text( + target_file("es", "seg.md"), + read_text(target_file("es", "seg.md")).replace( + "## Detalles\n", "## Detalles\n\nPárrafo añadido por un revisor.\n" + ), + ) + write_text(CONTENT_DIR / "seg.md", read_text(CONTENT_DIR / "seg.md").replace("here.", "now.")) + h.report = RunReport() + h.run(PlanOptions(file_filters=["seg.md"])) + r = h.report.lang("es") + t.check( + "a translation that no longer aligns is sent as a whole file, with the reason reported", + [c["id"] for c in h.client.orders[-1]["content"]] == ["s1"] + and r["whole_file"] + and r["whole_file"][0][1].startswith("unaligned"), + str(r["whole_file"]), + ) + + t.section("segment-level updates: fallbacks, context, response shapes") + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness(ratio=0.3) + seed_seg_translation(h) + write_text( + CONTENT_DIR / "seg.md", + SEG_MD.replace("explained.", "described in detail.") + .replace("stays.", "stays where it is.") + .replace("Refresh everything", "Refresh every table") + .replace("removed later", "removed soon"), + ) + reason = file_delta(h.config, "es", "seg.md", "outdated", load_status("es")["files"]["seg.md"], PlanOptions()) + t.check("mostly changed text is sent as a whole file", str(reason).startswith("mostly changed"), str(reason)) + t.check( + "--force sends the whole file", + file_delta(h.config, "es", "seg.md", "outdated", {}, PlanOptions(force=True)) is None, + ) + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness(context=-1) + seed_seg_translation(h) + write_text(CONTENT_DIR / "seg.md", SEG_MD.replace("**Ctrl+R**", "**Ctrl+Shift+R**")) + h.client.deliveries["es/seg.md"] = {"b1": "Abre el cuadro de diálogo con **Ctrl+Shift+R**."} + h.report = RunReport() + h.run(PlanOptions(file_filters=["seg.md"])) + order = h.client.orders[-1] + context_ids = [c["id"] for c in order["content"] if not c["translatable"]] + t.check( + "contextSegments -1 sends every unchanged translatable block as context; only the change is counted", + context_ids == ["c0", "c2", "c3", "c4", "c6", "c10", "c11", "c12"] + and [c["id"] for c in order["content"] if c["translatable"]] == ["b1"] + and h.report.lang("es")["chars"] == len("Open the dialog with **Ctrl+Shift+R**.") + and "**Ctrl+Shift+R**" in read_text(target_file("es", "seg.md")), + str(context_ids), + ) + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness(context=1) + seed_seg_translation(h) + write_text(CONTENT_DIR / "seg.md", SEG_MD.replace("**Ctrl+R**", "**Ctrl+Shift+R**")) + h.client.deliveries["es/seg.md"] = {"b1": "Abre el cuadro de diálogo con **Ctrl+Shift+R**.", "c0": "IGNORED"} + h.run(PlanOptions(file_filters=["seg.md"])) + order = h.client.orders[-1] + t.check( + "context segments are sent untranslatable and never written", + [(c["id"], c["translatable"]) for c in order["content"]] == [("c0", False), ("b1", True), ("c2", False)] + and "IGNORED" not in read_text(target_file("es", "seg.md")) + and "**Ctrl+Shift+R**" in read_text(target_file("es", "seg.md")), + str(order["content"]), + ) + commit_all("context run") + h.client.translatable_only = True + write_text(CONTENT_DIR / "seg.md", read_text(CONTENT_DIR / "seg.md").replace("stays.", "stays here.")) + h.client.deliveries["es/seg.md"] = {"b12": "El párrafo final se queda aquí."} + h.run(PlanOptions(file_filters=["seg.md"])) + t.check( + "a response with items for translatable segments only is mapped", + read_text(target_file("es", "seg.md")).endswith("El párrafo final se queda aquí.\n") + and "seg.md" not in load_status("es")["failures"], + str(load_status("es")["failures"]), + ) + + t.section("segment-level updates: JSON keys and toc names") + with git_mini_repo(): + h = seg_harness() + h.client.deliveries["es/_ui-strings.json"] = '{\n "greeting": "Hola {{name}}",\n "bye": "Adiós"\n}\n' + h.client.deliveries["es/toc.yml"] = json.dumps({"n0": "Inicio", "n2": "Guía"}) + h.run(PlanOptions(file_filters=["_ui-strings.json", "toc.yml"])) + commit_all("Spanish") + write_text( + CONTENT_DIR / "_ui-strings.json", + '{\n "greeting": "Hello {name}",\n "bye": "Goodbye",\n "extra": "Extra {count}"\n}\n', + ) + write_text( + CONTENT_DIR / "toc.yml", + "- name: Home\n href: index.md\n- name: Reference\n href: ref.md\n- name: Guide\n href: guide.md\n", + ) + commit_all("English v2") + h.client.deliveries["es/_ui-strings.json"] = { + "json": '{\n "bye": "Hasta luego",\n "extra": "Extra {{count}}"\n}\n' + } + h.client.deliveries["es/toc.yml"] = {"toc": json.dumps({"n2": "Referencia"})} + h.run(PlanOptions(file_filters=["_ui-strings.json", "toc.yml"])) + json_order = next(o for o in h.client.orders[::-1] if o["id_content"] == "es/_ui-strings.json") + toc_order = next(o for o in h.client.orders[::-1] if o["id_content"] == "es/toc.yml") + t.check( + "JSON order holds only the changed and the new key", + json.loads(decode_placeholders(json_order["content"][0]["text"])) + == {"bye": "Goodbye", "extra": "Extra {count}"}, + str(json_order["content"]), + ) + t.check( + "JSON rebuilt with the kept translation and the new ones", + read_text(target_file("es", "_ui-strings.json")) + == '{\n "greeting": "Hola {name}",\n "bye": "Hasta luego",\n "extra": "Extra {count}"\n}\n', + read_text(target_file("es", "_ui-strings.json")), + ) + t.check( + "toc order holds only the new name; toc rebuilt with the kept names", + json.loads(toc_order["content"][0]["text"]) == {"n2": "Reference"} + and read_text(target_file("es", "toc.yml")) + == "- name: Inicio\n href: index.md\n- name: Referencia\n href: ref.md\n- name: Guía\n href: guide.md\n", + read_text(target_file("es", "toc.yml")), + ) + commit_all("Spanish v2") + write_text(CONTENT_DIR / "_ui-strings.json", '{\n "greeting": "Hello {name}",\n "bye": "Goodbye"\n}\n') + sent = len(h.client.orders) + h.run(PlanOptions(file_filters=["_ui-strings.json"])) + t.check( + "a removed key is dropped without a request", + len(h.client.orders) == sent + and read_text(target_file("es", "_ui-strings.json")) + == '{\n "greeting": "Hola {name}",\n "bye": "Hasta luego"\n}\n', + read_text(target_file("es", "_ui-strings.json")), + ) + + t.section("segment-level updates: code and link target changes are repaired without a request") + with git_mini_repo(): + h = seg_harness() + index_es = BOM + "---\ntitle: Inicio\n---\n# Inicio\n\nIntroducción breve con `code` y [enlace](guide.md).\n" + h.client.deliveries["es/index.md"] = mask_code(index_es)[0] + h.run(PlanOptions(file_filters=["index.md"])) + commit_all("Spanish") + write_text( + CONTENT_DIR / "index.md", + MINI_FILES["index.md"].replace("`code`", "`code2`").replace("guide.md", "guide2.md"), + ) + sent = len(h.client.orders) + h.run(PlanOptions(file_filters=["index.md"])) + t.check( + "translation kept, inline code and link target taken from the new English, nothing sent", + len(h.client.orders) == sent + and read_text(target_file("es", "index.md")) + == index_es.replace("`code`", "`code2`").replace("guide.md", "guide2.md"), + read_text(target_file("es", "index.md")), + ) + commit_all("code and link") + write_text(CONTENT_DIR / "index.md", MINI_FILES["index.md"].replace("Short intro", "Long intro")) + reason = file_delta( + seg_harness(ratio=1.0).config, + "es", + "index.md", + "outdated", + load_status("es")["files"]["index.md"], + PlanOptions(), + ) + t.check( + "a prose change in the same block is translated", + isinstance(reason, Delta) and [i["id"] for i in reason.items] == ["b1"], + str(reason), + ) + write_text(target_file("es", "index.md"), read_text(target_file("es", "index.md")).replace("](guide2.md)", "")) + reason = file_delta( + seg_harness(ratio=1.0).config, + "es", + "index.md", + "outdated", + load_status("es")["files"]["index.md"], + PlanOptions(), + ) + t.check( + "a translation that does not verify against its own English is sent as a whole file", + str(reason).startswith("existing translation does not verify against its English (link targets"), + str(reason), + ) + + t.section("segment-level updates: source version lookup and --migrate-blobs") + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness() + seed_seg_translation(h) + status = load_status("es") + recorded_blob = status["files"]["seg.md"].pop("sourceBlob") + save_status("es", status) + write_text(CONTENT_DIR / "seg.md", SEG_MD.replace("**Ctrl+R**", "**Ctrl+Shift+R**")) + commit_all("English v2") + entry = load_status("es")["files"]["seg.md"] + t.check( + "an entry without sourceBlob finds the English version by its hash in git history", + isinstance(file_delta(h.config, "es", "seg.md", "outdated", entry, PlanOptions()), Delta), + ) + wrong = dict(entry, sourceBlob=git("hash-object", "content/seg.md")) + t.check( + "a sourceBlob whose content does not match sourceHash is ignored in favour of the lookup", + recorded_source_text("seg.md", wrong) == SEG_MD, + ) + t.check( + "no English version with the hash -> whole file", + file_delta(h.config, "es", "seg.md", "outdated", dict(entry, sourceHash="sha256:0"), PlanOptions()) + == "English version of the translation not found in git", + ) + out = captured(lambda: cmd_migrate_blobs(["es"], h.sources)) + t.check( + "--migrate-blobs records the blob of the version the translation was made from", + load_status("es")["files"]["seg.md"].get("sourceBlob") == recorded_blob + and "es: sourceBlob recorded for 1," in out, + out, + ) + out = captured(lambda: cmd_migrate_blobs(["es"], h.sources)) + t.check("--migrate-blobs keeps valid blobs", "recorded for 0," in out, out) + with mini_repo(): + t.expect_exit( + "--migrate-blobs outside a git work tree exits", + lambda: cmd_migrate_blobs(["es"], {}), + "needs a git work tree", + ) + t.check( + "outside a git work tree no sourceBlob is recorded", + source_entry("index.md", get_scoped_sources(TranslationConfig(mini_build_config()))["index.md"], "x").get( + "sourceBlob" + ) + is None, + ) + + +def assembled(delta: Delta | str, translations: dict[str, str]) -> tuple[str, list[str]]: + if isinstance(delta, str): + return f"<{delta}>", [] + text, problems, _, _ = delta.assemble(translations) + return text, problems + + +def selftest_segment_edge_cases(t: SelfTest) -> None: + t.section("segment-level updates: edge cases") + long_para = "Paragraph with enough words to pass the identity threshold of letters. " * 4 + english = f"# Title\n\n{long_para}\n\nSecond paragraph.\n" + t.check( + "a translation that is the English text (local --sync fallback copy) is sent whole", + md_delta(english.replace("Second", "Next"), english, english) == "existing translation is the English text" + and json_delta('{"a": "Hi there"}', '{"a": "Hi"}', '{"a": "Hi"}') == "existing translation is the English text" + and toc_delta("- name: B\n", "- name: A\n", "- name: A\n") == "existing translation is the English text", + ) + delta = md_delta( + "Use `a` and `b` there.\n\nThen `b` and `c`.\n", + "Use `a` and `b` here.\n\nThen `b` and `c`.\n", + "Usa `a` y `b` aquí.\n\nLuego `b` y `c` con `d`.\n", + ) + text, problems = assembled(delta, {"b0": "Usa {{c0}} y {{c1}} y `true` allí."}) + t.check( + "an extra span in a new block never shifts spans into a reused block", + text == "Usa `a` y `b` y `true` allí.\n\nLuego `b` y `c` con `d`.\n" and not problems, + f"{text!r} {problems}", + ) + t.check( + "a translation that lost a code span is sent whole", + str(md_delta("Use `y` here.\n", "Use `x` here.\n", "Usa x aquí.\n")).startswith( + "existing translation does not verify against its English (inline code spans 1 -> 0" + ), + ) + delta = md_delta("Run `b` before `a`.\n", "Run `a` before `b`.\n", "Ejecuta `a` antes de `b`.\n") + t.check( + "spans reordered in the English are mapped by value into the translation", + isinstance(delta, Delta) and not delta.items and assembled(delta, {})[0] == "Ejecuta `b` antes de `a`.\n", + str(assembled(delta, {})), + ) + zh_old, zh_new = "Use `a` then `b`.\n", "Use `a` then `c`.\n" + delta = md_delta(zh_new, zh_old, "先 `b` 后 `a`。\n") + t.check( + "a changed span lands where the translation placed the old one", + isinstance(delta, Delta) and not delta.items and assembled(delta, {})[0] == "先 `c` 后 `a`。\n", + str(assembled(delta, {})), + ) + text, problems, _, _ = finalize_block(Block("p", " Detail one.\n"), "Detalle uno.") + t.check( + "indentation of a block inside a list is restored", text == " Detalle uno.\n" and not problems, repr(text) + ) + text, problems, _, _ = finalize_block(Block("p", "Text.\n"), " \nTexto.\n ") + t.check("whitespace-only lines around a delivery are dropped", text == "Texto.\n" and not problems, repr(text)) + delta = md_delta("First.\n\nLast.\n\nAdded.\n", "First.\n\nLast.\n", "Primero.\n\nÚltimo.") + text, problems = assembled(delta, {"b2": "Añadido."}) + t.check( + "a block appended after a translation without a final newline stays a separate block", + text == "Primero.\n\nÚltimo.\n\nAñadido.\n" and not problems, + f"{text!r} {problems}", + ) + delta = md_delta( + "1. One.\n2. New.\n3. Two.\n4. Three.\n", "1. One.\n2. Two.\n3. Three.\n", "1. Uno.\n2. Dos.\n3. Tres.\n" + ) + text, problems = assembled(delta, {"b1": "2. Nuevo."}) + t.check( + "a step inserted into a numbered list sends only the new step; later steps are renumbered", + isinstance(delta, Delta) + and [i["id"] for i in delta.items] == ["b1"] + and text == "1. Uno.\n2. Nuevo.\n3. Dos.\n4. Tres.\n" + and not problems, + f"{text!r} {problems}", + ) + delta = toc_delta( + "- name: Overview\n href: b.md\n- name: Overview\n href: a.md\n", + "- name: Overview\n href: a.md\n- name: Overview\n href: b.md\n", + "- name: Resumen A\n href: a.md\n- name: Resumen B\n href: b.md\n", + ) + t.check( + "toc entries with the same name are matched by href when they move", + isinstance(delta, Delta) + and not delta.changed + and assembled(delta, {})[0] == "- name: Resumen B\n href: b.md\n- name: Resumen A\n href: a.md\n", + str(assembled(delta, {})), + ) + delta = md_delta("# T\n\nText changed.\n", "# T\n\nText.\n", '\n# T es\n\nTexto.\n') + t.check( + "bookmark anchors injected by a local build do not break alignment", + assembled(delta, {"b1": "Texto cambiado."}) == ("# T es\n\nTexto cambiado.\n", []), + str(assembled(delta, {"b1": "Texto cambiado."})), + ) + with git_mini_repo({"seg.md": SEG_MD}): + h = seg_harness() + seed_seg_translation(h) + uncommitted = SEG_MD.replace("**Ctrl+R**", "**Ctrl+Shift+R**") + write_text(CONTENT_DIR / "seg.md", uncommitted) + h.client.deliveries["es/seg.md"] = {"b1": "Abre el cuadro de diálogo con **Ctrl+Shift+R**."} + h.run(PlanOptions(file_filters=["seg.md"])) + write_text(CONTENT_DIR / "seg.md", uncommitted.replace("stays.", "stays here.")) + t.check( + "the English of an uncommitted run stays readable as the base of the next diff", + recorded_source_text("seg.md", load_status("es")["files"]["seg.md"]) == uncommitted, + ) + + +def selftest_code_placeholders(t: SelfTest) -> None: + t.section("code placeholders (the service turns backticks into quotation marks)") + body = FIXTURE_MD.lstrip(BOM) + masked, spans = mask_code(body) + t.check( + "fenced blocks and inline code become {{fN}} / {{cN}}; nothing with a backtick is sent", + "`" not in masked + and spans + == [ + "`View → Options`", + "`Selected.Tables`", + "```csharp\n// Assembly references must be at the very top\nvar x = 1;\n```", + ] + and "{{c0}}" in masked + and "{{f2}}\n" in masked, + f"{masked!r} {spans}", + ) + t.check("mask + unmask round-trips byte-for-byte", unmask_code(masked, spans) == (body, [])) + t.check( + "quotation marks the service put around a placeholder are dropped", + unmask_code("Abre «{{c0}}», “{{ c1 }}” y {{c2}}.", ["`a`", "`b`", "`c`"]) == ("Abre `a`, `b` y `c`.", []), + ) + t.check( + "unpaired quotation marks are kept", + unmask_code("«{{c0}}” y {{c1}}", ["`a`", "`b`"])[0] == "«`a`” y `b`", + ) + t.check( + "a missing, repeated or unknown placeholder is a problem", + unmask_code("{{c0}}", ["`a`", "`b`"])[1] == ["code placeholders missing in the delivery: [1] (1 of 2)"] + and unmask_code("{{c0}} {{c0}}", ["`a`"])[1] == ["code placeholders repeated in the delivery: [0]"] + and unmask_code("{{c0}} {{c7}}", ["`a`"])[1] == ["unknown code placeholders in the delivery: ['{{c7}}']"], + ) + content, _, info = prepare_payload("x.md", FIXTURE_MD) + t.check( + "Markdown payloads are masked; the job keeps the code", + "`" not in content and info.get("codeSpans") == spans and info.get("mode") == "raw", + ) + t.check("JSON and toc payloads are not masked", "codeSpans" not in prepare_payload("a.json", FIXTURE_JSON)[2]) + + with mini_repo(): + h = prod_harness() + source = MINI_FILES["index.md"] + h.client.deliveries["es/index.md"] = ( + "---\ntitle: Inicio\n---\n# Inicio\n\nIntroducción breve con «{{c0}}» y [enlace](guide.md).\n" + ) + h.client.deliveries["es/guide.md"] = "# Guía\n\nUna guía breve.\n\n> [!NOTE]\n> Texto.\n" + h.run(PlanOptions(file_filters=["index.md", "guide.md"])) + index_order = next(str(o["content"][0]["text"]) for o in h.client.orders if o["id_content"] == "es/index.md") + t.check( + "whole file: the order carries placeholders; quoted placeholders come back as inline code", + "{{c0}}" in index_order + and "`" not in index_order + and read_text(target_file("es", "index.md")) + == source.replace("title: Home", "title: Inicio") + .replace("# Home", "# Inicio") + .replace("Short intro with `code` and [link]", "Introducción breve con `code` y [enlace]"), + read_text(target_file("es", "index.md")), + ) + h.client.deliveries["es/index.md"] = "---\ntitle: Inicio\n---\n# Inicio\n\nIntroducción breve con «code».\n" + h.run(PlanOptions(file_filters=["index.md"], force=True)) + status = load_status("es") + t.check( + "whole file: a delivery that lost a placeholder is rejected, the file kept", + "code placeholders missing" in status["failures"].get("index.md", {}).get("error", "") + and "`code`" in read_text(target_file("es", "index.md")), + str(status["failures"]), + ) + + delta = md_delta("Set `a` to `b` now.\n\nKeep.\n", "Set `a` to `b`.\n\nKeep.\n", "Pon `a` en `b`.\n\nMantener.\n") + t.check( + "segment: sent blocks carry placeholders", + isinstance(delta, Delta) and [str(i["text"]) for i in delta.items] == ["Set {{c0}} to {{c1}} now."], + str(delta), + ) + t.check( + "segment: quoted placeholders come back as inline code", + assembled(delta, {"b0": "Pon «{{c0}}» en «{{c1}}» ahora."}) == ("Pon `a` en `b` ahora.\n\nMantener.\n", []), + str(assembled(delta, {"b0": "Pon «{{c0}}» en «{{c1}}» ahora."})), + ) + t.check( + "segment: a block that lost a placeholder is a problem", + assembled(delta, {"b0": "Pon «a» en {{c1}} ahora."})[1] + == ["block 0: code placeholders missing in the delivery: [0] (1 of 2)"], + str(assembled(delta, {"b0": "Pon «a» en {{c1}} ahora."})), + ) + + +class FakeHttpResponse: + def __init__(self, body: str) -> None: + self.body = body + + def __enter__(self) -> FakeHttpResponse: + return self + + def __exit__(self, *_: object) -> None: + return None + + def read(self) -> bytes: + return self.body.encode("utf-8") + + +def fake_http_error(code: int, retry_after: str | None = None) -> urllib.error.HTTPError: + headers = http.client.HTTPMessage() + if retry_after is not None: + headers["Retry-After"] = retry_after + body = io.BytesIO(b'{"code": "E", "message": "m"}') + return urllib.error.HTTPError("https://api.example/v2/translate/ai", code, "error", headers, body) + + +def selftest_http_client(t: SelfTest) -> None: + t.section("HTTP client: retries, Retry-After, jitter, rejected key") + ok_body = '{"data": [{"id": "s1", "translation": "hola"}]}' + + def client_for(answers: list[Any], jitter: float = 0.5) -> tuple[TranslatedClient, list[float], list[int]]: + sleeps: list[float] = [] + calls: list[int] = [] + + def opener(_req: Any, timeout: float = 0) -> FakeHttpResponse: + calls.append(1) + answer = answers.pop(0) + if isinstance(answer, BaseException): + raise answer + return FakeHttpResponse(str(answer)) + + return TranslatedClient("https://api.example/v2/", "k", sleeps.append, opener, lambda: jitter), sleeps, calls + + order: dict[str, Any] = {"content": []} + client, sleeps, _ = client_for([fake_http_error(429, "7"), ok_body]) + t.check( + "429 waits the Retry-After seconds, then succeeds", + client.translate_ai(order)[0]["translation"] == "hola" and sleeps == [7.0], + str(sleeps), + ) + client, sleeps, _ = client_for([fake_http_error(503, "600"), ok_body]) + client.translate_ai(order) + t.check("Retry-After is capped", sleeps == [MAX_RETRY_DELAY], str(sleeps)) + client, sleeps, _ = client_for([fake_http_error(408), fake_http_error(500), ok_body]) + client.translate_ai(order) + t.check( + "408 is retried; backoff is jittered between half and all of 2, 4, ... s", sleeps == [1.5, 3.0], str(sleeps) + ) + client, sleeps, calls = client_for([fake_http_error(401)]) + t.expect_raises("401 raises TranslatedAuthError", lambda: client.translate_ai(order), TranslatedAuthError) + t.check("a rejected key is not retried", sleeps == [] and len(calls) == 1) + client, sleeps, calls = client_for([fake_http_error(400)]) + raised: BaseException | None = None + try: + client.translate_ai(order) + except TranslatedApiError as e: + raised = e + t.check( + "400 is not retried and is not an auth error", + isinstance(raised, TranslatedApiError) and not isinstance(raised, TranslatedAuthError) and len(calls) == 1, + ) + now = datetime(2026, 1, 1, tzinfo=UTC) + t.check( + "Retry-After as an HTTP date; unparsable values ignored", + parse_retry_after("Thu, 01 Jan 2026 00:00:30 GMT", now) == 30.0 + and parse_retry_after("soon", now) is None + and parse_retry_after(None) is None, + ) + t.check( + "backoff without Retry-After stays within 60 s", + retry_delay(2, None, lambda: 0.0) == 4.0 + and retry_delay(2, None, lambda: 1.0) == 8.0 + and retry_delay(10, None, lambda: 1.0) == MAX_RETRY_DELAY, + ) + + +def selftest_merge_status(t: SelfTest) -> None: + t.section("--merge-status: status files merged entry by entry") + + def entry(source_hash: str) -> dict[str, Any]: + return {"sourceHash": source_hash, "status": STATUS_TRANSLATED} + + with git_mini_repo(): + files = {rel: entry(f"{rel[0]}0") for rel in ("a.md", "b.md", "c.md", "d.md")} + save_status("es", {"language": "es", "files": files, "failures": {}}) + commit_all("base") + main = git("rev-parse", "HEAD") + git("checkout", "-q", "-b", "localization") + status = load_status("es") + status["files"]["c.md"] = entry("c1") + status["files"]["e.md"] = entry("e1") + del status["files"]["d.md"] + status["failures"]["b.md"] = {"error": "x", "sourceHash": "b0"} + save_status("es", status) + commit_all("translation branch") + git("checkout", "-q", main) + status = load_status("es") + status["files"]["b.md"] = entry("b1") + status["files"]["c.md"] = entry("c2") + status["files"]["f.md"] = entry("f1") + save_status("es", status) + commit_all("main") + ours = git("rev-parse", "HEAD") + # Worst case of the fold: a conflict resolved by taking the branch's localizedContent as-is. + git("checkout", "localization", "--", status_path("es").as_posix()) + captured(lambda: cmd_merge_status(["es"], ours, "localization")) + merged = load_status("es") + t.check( + "entries the branch changed, added or removed come from the branch; the others from main", + merged["files"] + == { + "a.md": entry("a0"), + "b.md": entry("b1"), + "c.md": entry("c1"), + "e.md": entry("e1"), + "f.md": entry("f1"), + }, + str(merged["files"]), + ) + t.check("failures are merged the same way", set(merged["failures"]) == {"b.md"}, str(merged["failures"])) + + def cmd_self_test(config: TranslationConfig) -> int: t = SelfTest() selftest_corpus(t, config) selftest_fixtures(t) selftest_fake_client(t) + selftest_segments(t) + selftest_segment_updates(t) + selftest_segment_edge_cases(t) + selftest_code_placeholders(t) + selftest_http_client(t) + selftest_merge_status(t) print(f"\nself-test: {t.passed} passed, {len(t.failures)} failed") for name in t.failures: print(f" FAILED: {name}") @@ -2558,6 +4483,18 @@ def build_parser() -> argparse.ArgumentParser: actions.add_argument( "--baseline", action="store_true", help="mark existing, structurally valid translations as current" ) + actions.add_argument( + "--migrate-blobs", + action="store_true", + help="record the English git blob (sourceBlob) for status entries that only have a sourceHash", + ) + actions.add_argument( + "--merge-status", + nargs=2, + metavar=("OURS", "THEIRS"), + help="after merging translation branch THEIRS into OURS: rebuild each status file entry by entry, taking " + "THEIRS only where it changed an entry since the merge base (offline)", + ) actions.add_argument("--self-test", action="store_true", help="offline self-test (no key, no network)") actions.add_argument("--probe", action="store_true", help="check service types and languages against the API") actions.add_argument( @@ -2573,6 +4510,11 @@ def build_parser() -> argparse.ArgumentParser: "--retry-failed", action="store_true", help="re-plan files that failed at the current source hash" ) parser.add_argument("--report", help="write a markdown run summary to this path") + parser.add_argument( + "--keep-rejected", + metavar="DIR", + help="with --run: save billed deliveries that fail verification (order, response, problems) under DIR", + ) parser.add_argument( "--baseline-ref", help="git ref whose content/ hashes become the baseline " @@ -2593,6 +4535,8 @@ def build_parser() -> argparse.ArgumentParser: def check_option_combinations(args: argparse.Namespace) -> None: if args.repair_existing and not args.baseline: raise SystemExit("--repair-existing is only valid together with --baseline.") + if args.keep_rejected and not args.run: + raise SystemExit("--keep-rejected is only valid together with --run.") def main() -> int: @@ -2607,7 +4551,10 @@ def main() -> int: build_config = load_build_config() config = TranslationConfig( - build_config, os.environ.get("TRANSLATED_ENV"), os.environ.get("TRANSLATED_SERVICE_TYPE") + build_config, + os.environ.get("TRANSLATED_ENV"), + os.environ.get("TRANSLATED_SERVICE_TYPE"), + os.environ.get("TRANSLATED_CONTEXT_SEGMENTS"), ) if args.self_test: return cmd_self_test(config) @@ -2627,7 +4574,13 @@ def main() -> int: ) if args.plan: - cmd_plan(langs, sources, passthrough, options) + cmd_plan(config, langs, sources, passthrough, options) + return 0 + if args.migrate_blobs: + cmd_migrate_blobs(langs, sources) + return 0 + if args.merge_status: + cmd_merge_status(langs, *args.merge_status) return 0 if args.baseline: cmd_baseline(langs, sources, passthrough, args.baseline_ref, args.overwrite_baseline, args.repair_existing) @@ -2644,10 +4597,25 @@ def main() -> int: client = require_client(config, "--run") check_sandbox_filter(config, options, args.allow_unbounded) report = RunReport() + rejected_dir = Path(args.keep_rejected) if args.keep_rejected else None try: cmd_run( - config, client, langs, sources, passthrough, locales, options, args.allow_unbounded, args.lenient, report + config, + client, + langs, + sources, + passthrough, + locales, + options, + args.allow_unbounded, + args.lenient, + report, + rejected_dir, ) + except TranslatedAuthError as e: + report.notes.append(f"Run stopped: {e}") + print(f"Run stopped: {e}", file=sys.stderr) + return 1 finally: summary = report.to_markdown(config) print("\n" + summary) diff --git a/build_scripts/translation_segments.py b/build_scripts/translation_segments.py new file mode 100644 index 000000000..e5134d7a0 --- /dev/null +++ b/build_scripts/translation_segments.py @@ -0,0 +1,238 @@ +""" +Block segmentation and block-level diff of Markdown bodies for translate-content.py. + +A body (Markdown without frontmatter, LF line endings) is split into blocks: a blank line ends +a block; a heading, thematic break or table row is a block of its own; a list item, a `> [!` alert, a fenced +code block and an HTML comment start a new block. Blank lines after a block are kept verbatim +in its `gap`, so `join(segment(body))` returns the body unchanged. + +Two segmentations align when their block kinds are identical in order. A translation that was +written from English version O aligns with O, so the blocks of a newer English version N are +produced from three sources: the translated block of O (unchanged or moved text), a new +translation (changed or inserted text) or the English block itself (untranslatable text). +""" + +from __future__ import annotations + +import difflib +import hashlib +import re +from collections.abc import Callable +from dataclasses import dataclass + +FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,})") +HEADING_RE = re.compile(r"^#{1,6}\s+\S") +RULE_RE = re.compile(r"^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$") +LIST_ITEM_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+") +TABLE_ROW_RE = re.compile(r"^\s*\|") +TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|?\s*$") +ALERT_START_RE = re.compile(r"^\s*>\s*\[!") +COMMENT_START_RE = re.compile(r"^\s*" in line: + in_comment = False + flush() + continue + if not line.strip(): + flush() + if blocks: + blocks[-1].gap += line + else: + lead += line + continue + if FENCE_RE.match(line): + start(KIND_FENCE, line) + in_fence = True + elif COMMENT_START_RE.match(line): + start(KIND_COMMENT, line) + if "-->" in line: + flush() + else: + in_comment = True + elif HEADING_RE.match(line): + start(KIND_HEADING, line) + flush() + elif RULE_RE.match(line): + start(KIND_RULE, line) + flush() + elif TABLE_ROW_RE.match(line): + start(KIND_TABLE_ROW, line) + flush() + elif LIST_ITEM_RE.match(line): + start(KIND_LIST_ITEM, line) + elif ALERT_START_RE.match(line): + start(KIND_ALERT, line) + elif cur: + cur.append(line) + else: + start(KIND_PARAGRAPH, line) + flush() + return Segmented(lead, blocks) + + +@dataclass(frozen=True) +class Action: + kind: str + old_index: int = -1 + + +def diff_blocks(old: list[Block], new: list[Block], prose_key: Callable[[str], str] | None = None) -> list[Action]: + """One action per new block. Equal runs reuse the aligned old block; a changed or inserted + block whose exact text exists elsewhere in the old version (a move) reuses that block; inside + a replaced run, a block whose `prose_key` matches an old block of the same kind in the run + (only code, link targets, the list number or the heading level changed) is reused with repair; any other + untranslatable block (code, comment, table separator) is taken from the English.""" + first_index: dict[str, int] = {} + for i, b in enumerate(old): + first_index.setdefault(b.key, i) + matcher = difflib.SequenceMatcher(None, [b.key for b in old], [b.key for b in new], autojunk=False) + actions: list[Action] = [Action(ACTION_TRANSLATE)] * len(new) + for tag, i1, i2, j1, j2 in matcher.get_opcodes(): + for j in range(j1, j2): + if tag == "equal": + actions[j] = Action(ACTION_REUSE, i1 + (j - j1)) + elif new[j].key in first_index: + actions[j] = Action(ACTION_REUSE, first_index[new[j].key]) + if tag != "replace" or prose_key is None: + continue + runs = difflib.SequenceMatcher( + None, [prose_key(b.text) for b in old[i1:i2]], [prose_key(b.text) for b in new[j1:j2]], autojunk=False + ) + for sub_tag, a1, _a2, b1, b2 in runs.get_opcodes(): + if sub_tag != "equal": + continue + for k in range(b2 - b1): + i, j = i1 + a1 + k, j1 + b1 + k + if actions[j].kind == ACTION_TRANSLATE and new[j].translatable and old[i].kind == new[j].kind: + actions[j] = Action(ACTION_REPAIR, i) + return [ + Action(ACTION_ENGLISH) if a.kind == ACTION_TRANSLATE and not b.translatable else a + for a, b in zip(actions, new, strict=True) + ] + + +def reused_layout(actions: list[Action], new: Segmented, old_count: int, loc: Segmented) -> tuple[str, list[str]]: + """(lead, gaps) for the rebuilt body: the translation's blank lines between two blocks that + are both reused in their old order (and before the first / after the last), else the English.""" + gaps: list[str] = [] + for j, (a, b) in enumerate(zip(actions, new.blocks, strict=True)): + nxt = actions[j + 1] if j + 1 < len(actions) else None + follows = (nxt is None and a.old_index == old_count - 1) or ( + nxt is not None and nxt.kind in REUSED and nxt.old_index == a.old_index + 1 + ) + gaps.append(loc.blocks[a.old_index].gap if a.kind in REUSED and follows else b.gap) + starts_reused = bool(actions) and actions[0].kind in REUSED and actions[0].old_index == 0 + return (loc.lead if starts_reused or not actions else new.lead), gaps + + +def context_indices(actions: list[Action], blocks: list[Block], width: int) -> list[int]: + """Indices of up to `width` translatable, unchanged blocks on each side of every changed block; + a negative width selects every translatable, unchanged block (the whole page as context).""" + changed = {j for j, a in enumerate(actions) if a.kind == ACTION_TRANSLATE} + if width == 0 or not changed: + return [] + if width < 0: + return [k for k, b in enumerate(blocks) if k not in changed and b.translatable] + picked: set[int] = set() + for j in changed: + for step in (-1, 1): + k, taken = j + step, 0 + while 0 <= k < len(blocks) and taken < width: + if k not in changed and blocks[k].translatable: + picked.add(k) + taken += 1 + k += step + return sorted(picked) diff --git a/localizedContent/es/.translation-status.json b/localizedContent/es/.translation-status.json index 76a8f672d..d3b9c9008 100644 --- a/localizedContent/es/.translation-status.json +++ b/localizedContent/es/.translation-status.json @@ -4,374 +4,467 @@ "files": { "404.html": { "sourceHash": "sha256:8e5748cfcc9ca6d991e8e65c36fb2b411cfec8c78e4f7c7be154ca6200c2f80e", + "sourceBlob": "9d9a67896827224867051218fe748baeea2b7f81", "status": "translated" }, "_ui-strings.json": { "sourceHash": "sha256:01a51cba4185663722f9194cdaf9c8c61d9d65dac841e841120c8661b7485fbf", + "sourceBlob": "6bc4e98388d99e464e12eef2a70520fc9e10baeb", "status": "translated" }, "features/advanced-refresh.md": { "sourceHash": "sha256:3d00373433d7f4f8e01331d50c88cd8e11789d9114235a152cccf67ae4800397", + "sourceBlob": "595d6fef56d10834789d1f8265472e19ae61ba47", "status": "translated" }, "features/ai-assistant.md": { "sourceHash": "sha256:f408187864a033df9803ec001397d671d7d2661b82c6aefcdae02e4bf77e22fc", + "sourceBlob": "bddd06ecdb43cbd9bc78b18c27495bc413718fc8", "status": "translated" }, "features/ai-audit-log.md": { "sourceHash": "sha256:b6c0f43580208c5b37cebbeb8cf7937549023f062d3ab2e0700c73b812f365e9", + "sourceBlob": "b99d2865b00c29035f9e1008a651b3ab013ff53f", "status": "translated" }, "features/auto-reload.md": { "sourceHash": "sha256:12a094cafa3b8bcd0d1143ab195cd05c04cce7aceda6d897e9c63276420524fe", + "sourceBlob": "f3813a79cc79cfbf7eb5f4ad66753061c005cec1", "status": "translated" }, "features/Best-Practice-Analyzer.md": { "sourceHash": "sha256:9b948cd95b2378a11d35a1944861ee43f1ea5baa6ccad09e00654e32055e9f5d", + "sourceBlob": "dea840e3b90d43600b5a90815ea1882aec187eae", "status": "translated" }, "features/built-in-bpa-rules.md": { "sourceHash": "sha256:bdffc9c4bc7c941d725928c7b068e04907b5f111d273daacd59d38e57faf0a30", + "sourceBlob": "4028cb601ba4238559d596bf27c4834ae6bac40f", "status": "translated" }, "features/code-actions.md": { "sourceHash": "sha256:82baf7662cf3be05e7a4b399cd9cf3cf6b7ab76eb6eea7233b020be46862b47b", + "sourceBlob": "a7d72c1a74e36ad01c2ba50e5d249dbc24f60d7a", "status": "translated" }, "features/Command-line-Options.md": { "sourceHash": "sha256:510c1728baadeafa301f650793fa1ad76bb6c03b1e2039b2a4d6962ac6bba218", + "sourceBlob": "c2148da283dac1ebe67a27053a7032512a301d5e", "status": "translated" }, "features/connectivity/databricks.md": { "sourceHash": "sha256:9c696bd069c734d7e6b71f06ca44e4d1c5ab2eea9557350ede06b70f6ebc2ce1", + "sourceBlob": "ca07aacf80918aa83cfc89887d270cf03bb810a8", "status": "translated" }, "features/connectivity/fabric-onelake.md": { "sourceHash": "sha256:eeb03bfca0a8c72a8e89d0160bab987cd13e15b5c6d69824de871c68b445bc37", + "sourceBlob": "876294c93ac97b9cce6b29b03f97e5edea129035", "status": "translated" }, "features/connectivity/index.md": { "sourceHash": "sha256:ea13e9c16277ac6b1bc6fc627a8fd8e7fccfef1cd3cedcb0b0a355f5c468e42b", + "sourceBlob": "b7d0f59111937412b5a8751f2db13d576f2df168", "status": "translated" }, "features/connectivity/odbc.md": { "sourceHash": "sha256:3125fb8485c53abb96e4527f599f790f541b94b59dbd5cfff7e01c05a2a7e121", + "sourceBlob": "e49426d53974c10684ef4e52b507e5f6b242f58b", "status": "translated" }, "features/connectivity/oledb.md": { "sourceHash": "sha256:2152c2f02cb41fb7230b3ad5edbe87984fc3573c922cbc20696207eef97acfaa", + "sourceBlob": "596dae2c237609a3423f05f0c93a7a4d435c9ee2", "status": "translated" }, "features/connectivity/oracle.md": { "sourceHash": "sha256:243c9c3d888c4d0a3358e7504a6d75ed00143daf3650e8300b236e92ba5e5bd2", + "sourceBlob": "e6666fba1c9cc9df189efc83e69d83c0f3853a32", "status": "translated" }, "features/connectivity/power-bi-dataflows.md": { "sourceHash": "sha256:0b225765aba34b8efcb135fc393ab717c78bfbfda6510c7e9cefa52dac19c3e9", + "sourceBlob": "0c84343310f1d8541e0e79603c75877e272411f8", "status": "translated" }, "features/connectivity/snowflake.md": { "sourceHash": "sha256:f5abad907ade122a1002b86867ce69d207d051e4031d22693c65d410b79bb788", + "sourceBlob": "d6fd6fd3e1e0a95779907adbf7bd9c9de1c280d8", "status": "translated" }, "features/connectivity/sql-server.md": { "sourceHash": "sha256:44fa78e2f66178989be76fcf94cf80d0a898b789991a3891226dbe30c257fc37", + "sourceBlob": "566a9a774ac1b266a5c15fbb0132b5ecbfc3daa8", "status": "translated" }, "features/creating-macros.md": { "sourceHash": "sha256:b625c28e6d8e1c750f1cd082ec3a475ebcff8e39fd779818024e2bb3c1c10687", + "sourceBlob": "64736b7672327d8e1cf29c922a51b3f8f1baddc7", "status": "translated" }, "features/csharp-scripts.md": { "sourceHash": "sha256:aaf7aebdfc49037aae8aad82cb5d1b8540478a978c9fe3bc46a9acdb962bc63b", + "sourceBlob": "d04304d1be9ea2607b699cfa9aa662ef8020731b", "status": "translated" }, "features/CSharpScripts/Advanced/script-add-databricks-metadata-descriptions.md": { "sourceHash": "sha256:3e3502fa81133433b74714be43bbc8eb911a90f8848ff98d0f1828f56036f9f9", + "sourceBlob": "9f2f9a5953d82619aca182e7e07f1e52a2334c68", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-dlol-to-import.md": { "sourceHash": "sha256:d48972b9f153794a3c9d87f029978e777815eed865588eb6e54d87f2d83b40d5", + "sourceBlob": "2431a3db254213d085ebf92dbe7732fec85da63a", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-dlsql-to-dlol.md": { "sourceHash": "sha256:4680ff0aea6427e90a04380c7c09e6cb98484e75b110d4ac95330e639945d3c1", + "sourceBlob": "a2d82652bb9a738a0f3cf9294526a43b5ff91141", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-import-to-dlol.md": { "sourceHash": "sha256:db70a79ac1d2956f2b18a83b83c3693704eb1235b271697b020432e3d0a07990", + "sourceBlob": "98f5f70405bbec01c36fbafe83d0087d9e023a63", "status": "translated" }, "features/CSharpScripts/Advanced/script-count-things.md": { "sourceHash": "sha256:67e92ecd96ccbe2ec1373a8d3280182a126d832821001b70e21c990a4742deb2", + "sourceBlob": "da055364a63fc18aeec17804379b0a16eeacab65", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-and-replace-M-parameter.md": { "sourceHash": "sha256:0ce06f6f7719671d46c05fabbbc25a25721c9069615c6484b6dbe2bf058b8505", + "sourceBlob": "fa748cacfa71afe6e19286cfbb041e94cc409a48", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-databricks-relationships.md": { "sourceHash": "sha256:7100c6e93a2961e64c4962a5e22d0bca278b2f5c97b1c4509fbc20be236b7b5d", + "sourceBlob": "73730477e488ef890341f776cb7f7576ee312d3b", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-date-table.md": { "sourceHash": "sha256:a95991c83be2e3b7d7fbfa7c8b1eff5f59467487b66aaed2fe7a43e510f002ca", + "sourceBlob": "d214bca353a75f93a4ead8ffb10419acae0ab08e", "status": "translated" }, "features/CSharpScripts/Advanced/script-databricks-semantic-model-set-up.md": { "sourceHash": "sha256:d18ee6c4ecb762d70373d3632b828a41a5dd76b283a0f742065aed5bc9ce0233", + "sourceBlob": "4c3e4fc27c7263a2a08f8ea9dced2fd7f842be2c", "status": "translated" }, "features/CSharpScripts/Advanced/script-find-replace-selected-measures.md": { "sourceHash": "sha256:01f8139d9460fda1438695c2ff55c02a0671fc22ec442b111dd44236cd0f22c1", + "sourceBlob": "f80005ea3fb552e9a84cea242d690fa0389a7727", "status": "translated" }, "features/CSharpScripts/Advanced/script-format-power-query.md": { "sourceHash": "sha256:a18bc6f534e163a0cc87d75f47b02f13c3f6d47d4dd988ce53f55bd52002126b", + "sourceBlob": "c936182183ffab844cb70911d57f98ae6ce35153", "status": "translated" }, "features/CSharpScripts/Advanced/script-implement-incremental-refresh.md": { "sourceHash": "sha256:ebeaff67ce308880b8ba5be4b9cd198c30a5f48af87df7b477d48ed313bb1907", + "sourceBlob": "c61080670ead87cc586cdd71acaeffef655af7c1", "status": "translated" }, "features/CSharpScripts/Advanced/script-implement-user-defined-aggregations.md": { "sourceHash": "sha256:e4a1668f2fe37b9dfedf705f1ba2052a3f696c10e3422420ee24e7f9a983aa8c", + "sourceBlob": "c0fad152d810aff8d047fc97a1cdfc33dd2b75af", "status": "translated" }, "features/CSharpScripts/Advanced/script-output-things.md": { "sourceHash": "sha256:b610ce223647b635018c8e048c00b9004cc93ebc2309db26c30b383e9497b0fe", + "sourceBlob": "2a7b4d9fd9bf7aee8e1e3de2fbe07fc501bfacd8", "status": "translated" }, "features/CSharpScripts/Advanced/script-remove-measures-with-error.md": { "sourceHash": "sha256:d0a4c46a77e8af7bb00b109e8875d4b549bc0c03f22ec28d2f1bd2d95ce4f20d", + "sourceBlob": "0291bce2f09b6dcd79787ed8cbe3013fe3514f9f", "status": "translated" }, "features/CSharpScripts/Beginner/script-count-rows.md": { "sourceHash": "sha256:691212c34c7c4dc0b39809eedd600fa594cdcb8d8e0d116fa9e6804a07fd1088", + "sourceBlob": "843cec9bedafb0b804bd02bda264250c2b65bc69", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-field-parameter.md": { "sourceHash": "sha256:68a3acca96d3e372683c73129f700024e3229daa1752be3805653accc56fd91d", + "sourceBlob": "16b5b229f315eefaefabc80610a86ae119290c5b", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-m-parameter.md": { "sourceHash": "sha256:9e2f4adee2211c2ef4a90fb017b289dbe5bdd935e290172030e5b6f32f1bf2bb", + "sourceBlob": "7ddee7e50a4a7287e21f7a1e9e0c2ea0f6c8412a", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-measure-table.md": { "sourceHash": "sha256:0a641a1fa1d5b7b7ab73799b15d9434407b3fecc1d721ed1d43a58a4ff1d30e2", + "sourceBlob": "85dcab0922799ef70ded284fa5d15ca36994457f", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-sum-measures-from-columns.md": { "sourceHash": "sha256:50bd3f2a8ef34e438493f91da92be743f7cd4296a694d3243460abeb47f28c89", + "sourceBlob": "752a10e104f215457dc2d3c15eb2e55323aa4041", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-table-groups.md": { "sourceHash": "sha256:74dd9a2bda903d85972caedcf9e230ca4843fa87fa490bd82122498db8a261c4", + "sourceBlob": "7f89ea59485e72f3b39dbcfb854581bccf88e1e7", "status": "translated" }, "features/CSharpScripts/Beginner/script-display-unique-column-values.md": { "sourceHash": "sha256:9305105469bd45fb0ce7edbb66fdc9c7ec9be16d377bca79c8265ca16451aeeb", + "sourceBlob": "1dbaf8539bacd99ba9608b83094f0dbf16aca869", "status": "translated" }, "features/CSharpScripts/Beginner/script-edit-hidden-partitions.md": { "sourceHash": "sha256:fe1dfb281e64a7367180a7705a5559be43d8c9368e1c5e7acccab093ee1430d4", + "sourceBlob": "967b133b0bd871b6d17ed115a6c723f7c6075e45", "status": "translated" }, "features/CSharpScripts/Beginner/script-format-numeric-measures.md": { "sourceHash": "sha256:e2c47703f04f15d22d23fd66c8abda9a27e5ff15ffbf6c9e73a9dbc54ba3952d", + "sourceBlob": "8057806143c71360baf5228c35dad7b95208ade7", "status": "translated" }, "features/CSharpScripts/Beginner/script-show-data-source-dependencies.md": { "sourceHash": "sha256:6bd1324dda48a5363b2d1e9d9e893f61f2e3364913c64848180c5069442a1600", + "sourceBlob": "c95f1107c52231e316455c302c29650093e5c00b", "status": "translated" }, "features/CSharpScripts/csharp-script-library-advanced.md": { "sourceHash": "sha256:1bffc5de50a63a02a1499b6c690f8f021a456f69bcea94de8d30589792f76e9f", + "sourceBlob": "8dac2ac36c442c6d19cac96b775a2d447aa7531c", "status": "translated" }, "features/CSharpScripts/csharp-script-library-beginner.md": { "sourceHash": "sha256:8297f36b1b4788ff484bc7fbda33135465e258039c2c4fb91a10d7dd8d8dceb7", + "sourceBlob": "544e3bd8ad1015ddd1fdf1e20f4194d3b666b4b5", "status": "translated" }, "features/CSharpScripts/csharp-script-library.md": { "sourceHash": "sha256:5097363cc7a2ae52c4ad14596dfe2e963ba391ff21942bd0e472c45dca814ec7", + "sourceBlob": "052c9c79b00fac845af6faf3dfc417537b257859", "status": "translated" }, "features/CSharpScripts/Template/csharp-script-Template.md": { "sourceHash": "sha256:d33ce23f236296d5f6a22ccda044b9cc2cc97b36fac41cfb8413f76c85c56af6", + "sourceBlob": "11ad23eb33ddda31b959741c47ba60d8998bdfe8", "status": "translated" }, "features/Custom-Actions-hidden.md": { "sourceHash": "sha256:cce28f9efc24691ee1e3b025b1d5e87244c1dd656d8d4fe1226518fdd05fb40f", + "sourceBlob": "82152e8036fb5839a393b6dd935f69f079a3f16d", "status": "translated" }, "features/dax-debugger.md": { "sourceHash": "sha256:2edb8f85458de285384d61b1bd055fda7ff1f02e451a601cb555acac25c64f50", + "sourceBlob": "daa9f9eb4216be244ddabc99d2c24b79ee6adfac", "status": "translated" }, "features/dax-editor.md": { "sourceHash": "sha256:b8456895d28fb1c0ac0e2e78432a85e2d85bdf9ae2aa1e2d9c9edb2784e55923", + "sourceBlob": "b6dbdb572a7d9e9121ec6dc8e6d51f10018dadb5", "status": "translated" }, "features/dax-optimizer-integration.md": { "sourceHash": "sha256:7f360b8d39e0a763558717f52a112f4e908cd7e30b99c94580b0f478277858f1", + "sourceBlob": "187aca039f9ad1bf5d08fd48c59bc1c1cdbe973e", "status": "translated" }, "features/dax-package-manager.md": { "sourceHash": "sha256:deb8cbd2b8ad4c16f627cfd8b12f903da4b08287cf955f14cc1a8ecddfc7a825", + "sourceBlob": "f2b54efe53008a5ff258010a9b9c7dd8ba9ab035", "status": "translated" }, "features/dax-query.md": { "sourceHash": "sha256:43d7d61f433e79310e7af7d7e44e34ffa6f80b49df061b34d91f9285a55131d4", + "sourceBlob": "8c8f8290b7fc114f9a41133437cb64cb7541c74f", "status": "translated" }, "features/dax-scripts.md": { "sourceHash": "sha256:68a0f9afca10252a6d2c9e4c92642be0d725368a46af4c4a59b7420756d92d83", + "sourceBlob": "48ee7a19819e514fd13026e9a5abccd350531cb6", "status": "translated" }, "features/deployment.md": { "sourceHash": "sha256:ccf07d6fb575c9a62fa94e4e5b3ba9e41543a57b1b651441ecdde3eabe98b0f2", + "sourceBlob": "e8b2cef8f04a3e015825aab2432448e6763e29cf", "status": "translated" }, "features/hierarchical-display.md": { "sourceHash": "sha256:45963c4ab2a25c88ab2bcb5a8115e1569ce71098b36c09b4f3c7e8663dfb1f32", + "sourceBlob": "bcc131845c0deb904311df84339e39f4a6c657e2", "status": "translated" }, "features/import-tables.partial.md": { "sourceHash": "sha256:b416ea2b5d09ea7cc44d41ae621ca79a122104ad088f8c8bcd3c7d320ed88431", + "sourceBlob": "586e2c6455a8d676a85c09d69628d9d3f7006e9b", "status": "translated" }, "features/index.md": { "sourceHash": "sha256:523bfe7e887f482e3eac8691e77879feeac70030cd508acc736994622c6db683", + "sourceBlob": "9d66dff2ab0078ddefdc944fda46cd40ebb22951", "status": "translated" }, "features/mcp-server.md": { "sourceHash": "sha256:9fbfc101f7c6f4a71721ef6382610bdec5123aa3ddd0f4728ee93852bf4f8196", + "sourceBlob": "1915106ad79557a9fc86e7c87bc600f974e04249", "status": "translated" }, "features/metadata-translation-editor.md": { "sourceHash": "sha256:7dd826b950714972b3429347f09fed22bcdbaf3f149e4f47dc6382ecf26e69e5", + "sourceBlob": "0aadd2b697b21873edd16fbee8e453c8883b7138", "status": "translated" }, "features/perspective-editor.md": { "sourceHash": "sha256:d7bd4b9834bb8fac3a1072e4c84d43efe36d64e7443b50f0778f0f76f189476f", + "sourceBlob": "b823606a62a57145477463f74a99fec5abf35bf7", "status": "translated" }, "features/pivot-grid.md": { "sourceHash": "sha256:13469e84b4715d3989eb9f9220222807cad8eaa30bb5dc6eb1837da1e13d82ee", + "sourceBlob": "2ec2cc595eb7bce1320d14aee51d119bc658d104", "status": "translated" }, "features/refresh-overrides.md": { "sourceHash": "sha256:8ebe84c4db022e87ff73603eed7974660e6edab0fcc5a8f416b1290a6f05e413", + "sourceBlob": "953a055ad5f45c6d57947adba38711895375a8c6", "status": "translated" }, "features/save-to-folder.md": { "sourceHash": "sha256:a0787dde9336e2f631234d0d95ec8519ccdb84a1495325090c56e9ec8caeee08", + "sourceBlob": "32096acf2a61616cad34cfb6dc26be9b8c9828d4", "status": "translated" }, "features/save-with-supporting-files.md": { "sourceHash": "sha256:a1b88d752e28fed5063904efd4c743e043cb3fe28591b6cde5a8182d00130b41", + "sourceBlob": "42c8e358e5865899c7318909c20cfe907c2c7879", "status": "translated" }, "features/script-helper-methods.md": { "sourceHash": "sha256:5cb24ef463fa580075848ea2facacb0b906a1e08ff6cb1c62f7e41ae3824fbf6", + "sourceBlob": "0e6d57a40fa73f9c43cf764fc9d9534a2e913e74", "status": "translated" }, "features/semantic-bridge-metric-view-fields-and-dimensions.md": { "sourceHash": "sha256:58bd40eea2c9da6f1cf4b1b24ca50f04ed021a171289ba554edccd4874fbafa4", + "sourceBlob": "04720649dd3f611b9bd31a3e139dac9bea1321f4", "status": "translated" }, "features/semantic-bridge-metric-view-object-model.md": { "sourceHash": "sha256:6028084095ef69f4cd3c5f0e0770809dafca75ba3636e9f02335c7984e06c826", + "sourceBlob": "c6a1faafd947b0d7d74f86730285ee36cb63bac7", "status": "translated" }, "features/semantic-bridge-metric-view-tabular-translation.md": { "sourceHash": "sha256:74be78dd2533ebab27bb4d9dff59a263f12f19b90e8bc746d6206feb4d7f08b5", + "sourceBlob": "92ffa33e53833d234015db9a287e0e8a1d621ad2", "status": "translated" }, "features/semantic-bridge-metric-view-validation.md": { "sourceHash": "sha256:e2f6dddbee46537b3923de827539556524d25ad09b5125e2df1510eac271179c", + "sourceBlob": "0dbe93b10b900b5b49137651c119bd2877d30a7f", "status": "translated" }, "features/semantic-bridge.md": { "sourceHash": "sha256:148612d364d9d289779fdf6bb5119e69a5a06f1162e11a909c5456284fbfcdbb", + "sourceBlob": "14794316b4726ee655ed893d0fb88318451c4184", "status": "translated" }, "features/Semantic-Model/direct-lake-sql-model.md": { "sourceHash": "sha256:7f45a897ce4afe418c209f05ba10729e24bba2a60d8710191bfd66051336293b", + "sourceBlob": "ea02d4a245095ae863fc83d1f8efaea45008b918", "status": "translated" }, "features/Semantic-Model/direct-query-over-as.md": { "sourceHash": "sha256:280fbda0a57da7ba6b11294d3809f7b52c4f25cdf7ca5fa07e85264e5cd1e74d", + "sourceBlob": "9f0d50abd8e16a26b6ddce3b950b418e250b9df0", "status": "translated" }, "features/Semantic-Model/semantic-model-types.md": { "sourceHash": "sha256:c6bc2434213fc85c0062d07e80f8e0341d4375cce84561d770944e00a7f1b1d0", + "sourceBlob": "a7d91a750887b553c8810790fd860f03eefd5480", "status": "translated" }, "features/table-groups.md": { "sourceHash": "sha256:5459da04f54b462c03fc23a93c0ea70583db05f352b5d1fa4eff8de6fd789586", + "sourceBlob": "b15486c461048b998fde6c32bce176e758bdf2f3", "status": "translated" }, "features/table-preview.md": { "sourceHash": "sha256:919ddbf231d1336651625f666704024ac80ce09b637d0c73ccb94aba492afc9e", + "sourceBlob": "9d7d9bf467d7d9b87945f6318da3f2c158f3d1af", "status": "translated" }, "features/te-cli/includes/te-cli-preview-notice.md": { "sourceHash": "sha256:78d64bd2aeab6acb614ba5793a522de28cc7726655789e476ee50e267f561a46", + "sourceBlob": "e9dbcc25c59eec001190d9f25f394af5650861ce", "status": "translated" }, "features/te-cli/te-cli-auth.md": { "sourceHash": "sha256:7c546bae4a34fb5d356b48977274fdb0b106bc862f89d09b59fa857ac78a962e", + "sourceBlob": "3348d29f26b1a64a4154901896bb2f0195124d21", "status": "translated" }, "features/te-cli/te-cli-automation.md": { "sourceHash": "sha256:4816d4daf4c65c11daeb777419b0d539d79055120d6516ab432b7567961fc8f9", + "sourceBlob": "f67dfbba4a2cedad06b78a5f3cf17da282f5a858", "status": "translated" }, "features/te-cli/te-cli-cicd.md": { "sourceHash": "sha256:3407d54cf37c954a2e53d190c14f84502a04f602c3d1605d1e040f479eb57381", + "sourceBlob": "af1939b97325f2b52f820a21f08a53d50af5994e", "status": "translated" }, "features/te-cli/te-cli-commands.md": { "sourceHash": "sha256:3002131eec0fe7c2f8e318c6bdb7227792ff24970f88738da20a4e900af573eb", + "sourceBlob": "3e0f0ae6fa8f17b069755ac733e72e9a460b5076", "status": "translated" }, "features/te-cli/te-cli-config.md": { "sourceHash": "sha256:b9778fc392a9c0f2ec9631c4bda5a71fbb8161103102b2e10f923a0f68205a64", + "sourceBlob": "cb3f4b80a49df21e8f49aee372a54bb3a2a56ae5", "status": "translated" }, "features/te-cli/te-cli-findings.md": { "sourceHash": "sha256:7fbebd98ae9b0954f975e87b2d58ec49c888349ac4e51e5399fd87642dab8ce9", + "sourceBlob": "ebd0602375cc90e65a87a64313230be2c2336871", "status": "translated" }, "features/te-cli/te-cli-install.md": { "sourceHash": "sha256:3846cb039ee6af3f7839e1dc9c3eb4581ae64d35ebba3155ce3e5c0e1e112fe8", + "sourceBlob": "7e2c09d12d14142c566a1bc0f2f7f165f5e75a3e", "status": "translated" }, "features/te-cli/te-cli-interactive.md": { "sourceHash": "sha256:0c276940dcfc72aaed58827820de0b1e253113c0b10a97401cc5c44eb1928663", + "sourceBlob": "434b92b9def4a864b3aac654c1bf17a81faea23c", "status": "translated" }, "features/te-cli/te-cli-limitations.md": { "sourceHash": "sha256:03f4d5fd792465b3ba3aa6e3f731937ca34648c5a8820936ad2b432da7dd66de", + "sourceBlob": "11470df38041c9efb886d4f233b7016f233aa243", "status": "translated" }, "features/te-cli/te-cli-migrate.md": { "sourceHash": "sha256:1bb8b09850ad6534ba7da224d4fd26365b24f27ea1e181031619fdb926130478", + "sourceBlob": "92f4fc0f6b5a7ff13ecee6a6090d10c1f00cc721", "status": "translated" }, "features/te-cli/te-cli-skill.md": { "sourceHash": "sha256:55eb8144d60ffde111240da39e89c8ea98390d868e79363c70024ddad541d8a9", + "sourceBlob": "fe2cbae27138b4ffa18c36a2b5df4e85c2c75b9e", "status": "translated" }, "features/te-cli/te-cli.md": { "sourceHash": "sha256:3809890dbd0821611a2300d79384b21997019d0cccf0e713d81ff2862da558b0", + "sourceBlob": "8c5719ca18d018255956508c615509bc0d198e68", "status": "translated" }, "features/tmdl.md": { "sourceHash": "sha256:0200e992b3cc0f6ea5a18960a70bc722d4f6343d20001ec3ef23f724edbd180e", + "sourceBlob": "3dfd0667b205942cb0535306493253abc97adcab", "status": "translated" }, "features/toc.md": { @@ -380,170 +473,212 @@ }, "features/unsaved-changes.md": { "sourceHash": "sha256:b4276f4a2d3144459dfc5e6fa6f6e71d560547cad60cc6c6227c8a2207fc3651", + "sourceBlob": "01f50b550f63849d244f6006fe95f8bd8c19ef36", "status": "translated" }, "features/Useful-script-snippets.md": { "sourceHash": "sha256:c8a0ddfa6f8ceadc38a222a3c5155f2da992c93f1fe4297a629251f1b5b95c28", + "sourceBlob": "c6fc2d702972555e1b16467df49ee861f16b4e74", "status": "translated" }, "features/user-context-calculated-columns.md": { "sourceHash": "sha256:2d7a43f84729d3d925e4d918752c57a05335335e8af7547bc7f829a833e46751", + "sourceBlob": "50940f82680787205217026bca712a95e81ae285", "status": "translated" }, "features/using-bpa-sample-rules-expressions.md": { "sourceHash": "sha256:5f01197ad9e53d65cb6c7756372e4827a31e994a0a70a16a8aaded9b52339515", + "sourceBlob": "c1e81f45fef0c94967f1c517dc46dbbe3c3d8b97", "status": "translated" }, "features/using-bpa.md": { "sourceHash": "sha256:ea063d06bedae22da258863a7f4727edb1be9b9751602681d1a9a23d66950ee7", + "sourceBlob": "86c47854b91121eca8f2b754e32629daaad2640e", "status": "translated" }, "features/views/bpa-view.md": { "sourceHash": "sha256:dcccdbd044bbd074e52b5abf8e72d8ec7b714d81617f0261b49ab8a776de1670", + "sourceBlob": "f13334c63f552805fc2b3d50a215047a5fc4c82a", "status": "translated" }, "features/views/data-refresh-view.md": { "sourceHash": "sha256:6427f5aec9357680f68f96702ca980623b7f518f505ca0a179451aa63d701b15", + "sourceBlob": "8c9ef9694a63cec18b6fac328ee4a313c2f308f4", "status": "translated" }, "features/views/diagram-basics.partial.md": { "sourceHash": "sha256:6159325df46084d452d7adb3e28b85ba4da85bd270c9e158624ea2a16653d182", + "sourceBlob": "efdef982ec12e4c57b53f29fc58582d9f5ac36ab", "status": "translated" }, "features/views/diagram-view.md": { "sourceHash": "sha256:55ff4574e2a9a8bfc1f92025e34a95a42046daef4cf96315b7c64be03b935796", + "sourceBlob": "d9b5d85f347f739c28091049bb3b69fd3ddc4314", "status": "translated" }, "features/views/find-replace.md": { "sourceHash": "sha256:32a2e329d26e3712c1115ee0cb1965780387888d05c5af11b7bb2b056c8f3d42", + "sourceBlob": "e8ea546897ffdd36d31f043833f4cb1a374636ba", "status": "translated" }, "features/views/macros-view.md": { "sourceHash": "sha256:6f8046bc8d5bcf581c147a3263fefeb4f6dbc160cccd29a0a8b09db2949f055e", + "sourceBlob": "371f9dd56bdc565549d82dc6e330c8bed888b8b2", "status": "translated" }, "features/views/messages-view.md": { "sourceHash": "sha256:c444a771025eab4067d298298fa8391fd1e0157c38582320b097f9d2320d4657", + "sourceBlob": "a48bd92d4addbe14a03f74ce7afbadedbabea93f", "status": "translated" }, "features/views/properties-view.md": { "sourceHash": "sha256:3de52f392fb38a6066663c222690aa9b42ad3d611496e76463eed2c2c416faf6", + "sourceBlob": "0de8aa4db131f025fb45835d377d073d3df0bb30", "status": "translated" }, "features/views/tom-explorer-view.md": { "sourceHash": "sha256:620e8d14dba16e4b5bdbe6ad7beff5b79903b29551ca338efa94d6efaeb041a0", + "sourceBlob": "91969425bae2aed64fa70aaa2fe852341d14de15", "status": "translated" }, "features/views/user-interface.md": { "sourceHash": "sha256:7e6a431584ea6e620b932f5ff895cea2a9a28faecc253b212fe62b033ed833c7", + "sourceBlob": "10005d97d86dca7ddc750111b0862c0936c2cb43", "status": "translated" }, "features/Workspace-Database.md": { "sourceHash": "sha256:3e8d2cab08786abac392997434c23ebe54b1bd2b7e1158513819bf366060c0ee", + "sourceBlob": "50cffd07b076258eed8fd54b4fa71126fde31c57", "status": "translated" }, "features/workspace-mode.partial.md": { "sourceHash": "sha256:e05d93903abab9005d7a7d2b03dcb4f65b0e651c4456beef68750dfb44fabc2f", + "sourceBlob": "d7067641fdb0151f99050a21c69b16732db7355d", "status": "translated" }, "getting-started/app/index.html": { "sourceHash": "sha256:1d58f022bcf30ea4582377c3145def5edc9c8dd96c25abbb66a9ce4b6918c457", + "sourceBlob": "12c587ca046b180a307a4e66256db0319db5ac5c", "status": "translated" }, "getting-started/azure-marketplace.md": { "sourceHash": "sha256:aa36a1b4f471f78d88e57402405aec43b37ccd2d1a53a1444ab1731de18c88b1", + "sourceBlob": "f2d05b99fec5603416ae3a279026081a878d53d4", "status": "translated" }, "getting-started/boosting-productivity-te3.md": { "sourceHash": "sha256:6cbd04a7b267e22d41c812a2efedda6cfc840a8c9e7146b0144cae6a1b077a61", + "sourceBlob": "ff0edf619e6010c0a164b69d7781cec92c169845", "status": "translated" }, "getting-started/bpa.md": { "sourceHash": "sha256:dd602ec731cd29ebb700cf7377396fee262113d74c9c584d8dd17dd46c21fba1", + "sourceBlob": "14c8010c6f7ed5500f707a0b9e10a4d5c674cd03", "status": "translated" }, "getting-started/creating-and-testing-dax.md": { "sourceHash": "sha256:326a4af18f1bacea3e4b6a52d44c336bab3495074918c9cf49722d2e579a561f", + "sourceBlob": "dab9ba65e0500cb21f67b7a6218c83d5663e7197", "status": "translated" }, "getting-started/cs-scripts-and-macros.md": { "sourceHash": "sha256:4c587caf1e8d5a60f4ce6d1e6c9181ba8469b6b46c4dd315239689590ec4b135", + "sourceBlob": "f78d1ab47a62093e17dc33851af6f048962eb688", "status": "translated" }, "getting-started/dax-script-introduction.md": { "sourceHash": "sha256:1c500ed5798529eb123c3f097b3052dd48a97e2bb4a6fb8e2ff8a3c0660048ae", + "sourceBlob": "9afd91645a2433cf7b2e627e5a24e2705fd7e5ea", "status": "translated" }, "getting-started/desktop-limitations.md": { "sourceHash": "sha256:642629068eda1994deaeed8eb17e811e878c3f9fa4d2b73d20d0ca65e87568d5", + "sourceBlob": "80696f124337c358bdea791ac18aedb388a1eccd", "status": "translated" }, "getting-started/editions.md": { "sourceHash": "sha256:f7d969bcc3e0464cabd627045f03454978e6bb68ff4ffc754694dd6c89e07cd4", + "sourceBlob": "25c0ca619fda06ba57679a8ceaac6fc35d5742b3", "status": "translated" }, "getting-started/general-introduction.md": { "sourceHash": "sha256:310091474ffb682608d139342191400badf995a700cbc54e539b70e1fd879f79", + "sourceBlob": "cb8d9383005d76762c8ffa4bfd4f14d9f463c299", "status": "translated" }, "getting-started/Getting-Started-te2.md": { "sourceHash": "sha256:f7f3fc16ac57b499364b970568de8ccd8b6fd1d2122ddcfb4bca709ac5af9776", + "sourceBlob": "645a3670337ebca30537d0a4013f0ad0d08b9bd2", "status": "translated" }, "getting-started/getting-started.md": { "sourceHash": "sha256:a0265501f2a95f673ce169db86208950eec6e2523d1d4f3e0d241b3372d53e96", + "sourceBlob": "d703d438dee570508d4e6bc3e223975be91b9530", "status": "translated" }, "getting-started/github-flow.md": { "sourceHash": "sha256:8a6cff7e623f0d857474b867d285f3a408f81023160652a167ed79531037343a", + "sourceBlob": "a80125cade3d4f2402ffd4a7629c267922f96492", "status": "translated" }, "getting-started/importing-tables-data-modeling.md": { "sourceHash": "sha256:43389d4ff2a11ad55cbcebbcc1550cfdd551c32fbe31dc2f5606340e4c3e57d8", + "sourceBlob": "64f9ac7f1bb6d128a2740ddede62941604171ece", "status": "translated" }, "getting-started/index.md": { "sourceHash": "sha256:04757c50e64426cab3b67466495644cfbd6bd3bfed8379473d675e5bc2ca4ebd", + "sourceBlob": "5c4f8089e16f0b76bfd1a176bd640da97a30c6e4", "status": "translated" }, "getting-started/installation.md": { "sourceHash": "sha256:ef3f254c8f8def44e84251f71974aec719f3b23b69dd37d3c5bc343b3a53ed8a", + "sourceBlob": "2435ed67e1a53b4e2e21f968e9e469187862fa6c", "status": "translated" }, "getting-started/migrate-from-desktop.md": { "sourceHash": "sha256:073cd52677a718dba308dfac64ba1f27b76ba4534f77ddb99b2b4447c1511f70", + "sourceBlob": "b1fc8b39d6efcdfb61f7ac50beece138b8666a16", "status": "translated" }, "getting-started/migrate-from-te2.md": { "sourceHash": "sha256:e3b8f23bb9f7f7e7e0a911db6d0de1ad7f6479cdeed5e6093421e27ff6ce3c20", + "sourceBlob": "487a4e16fdd4dd8b6ac57331a6ef68c5155cac28", "status": "translated" }, "getting-started/migrate-from-vs.md": { "sourceHash": "sha256:e828a626484397cb46e2a36b35675c639dbdd5e17976c21967f337deafbb9f4f", + "sourceBlob": "219da009cff71c8ac336668cedead910296cd0cb", "status": "translated" }, "getting-started/optimizing-workflow-workspace-mode.md": { "sourceHash": "sha256:be881580d2cef038fc74e503591f8110998d40922a7c30bc15936ad7a36dc823", + "sourceBlob": "3a930229e6e6fc71e2ffcaf87336a8b56a9efd07", "status": "translated" }, "getting-started/parallel-development.md": { "sourceHash": "sha256:25d9494276172d9ffcdb40b37e6d5cceee10afd48c413a7299e5f686be0fb613", + "sourceBlob": "71cb81c4c14c4e69c7e259f8312f4fb2290e4c95", "status": "translated" }, "getting-started/personalizing-te3.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:0e2b6a13cb3a279d40f713d56287aca1a7d6d340ebec4e13842b3d51b42f76d8", + "sourceBlob": "8f3836fd27c04e5868769a605ebafa53aa37b130", + "status": "translated" }, "getting-started/Power-BI-Desktop-Integration.md": { "sourceHash": "sha256:f725405cfbeeb34f15100aa477e813197a3d40640fdcd2de82f2262cf3b41ef8", + "sourceBlob": "94d771d91a0b9665b82941bbe23e530e73494b03", "status": "translated" }, "getting-started/refresh-preview-query.md": { "sourceHash": "sha256:3bdec39f290189d4f7a9e60eebcaf4eed6a54db750203a2d1bf692927d4fa5c7", + "sourceBlob": "013c21f02eca15be302eca2ce4059b0850003091", "status": "translated" }, "getting-started/sharing-macros-bpa-rules.md": { "sourceHash": "sha256:a2c174930bce002fa8691b6585917c06845079b0f8cd1b03133707c45fddf91d", + "sourceBlob": "9b691989ed5f710213f587085944acb6c66ef976", "status": "translated" }, "getting-started/toc.md": { @@ -552,242 +687,302 @@ }, "getting-started/training-telearn.md": { "sourceHash": "sha256:cba2b907335edf7e1310d6323c063305cbed3c66013758b932d91466394b4d85", + "sourceBlob": "b397eab6d24a427124a93029d1ca1291c697c75a", "status": "translated" }, "getting-started/views/bpa-view-reference.md": { "sourceHash": "sha256:15ca9b6b41ad2b4a120b6ef5ea17c4474e72fa68375e5539654e662057eb2383", + "sourceBlob": "9c7382004050c2209a0562f6849ff164472fb554", "status": "translated" }, "getting-started/views/data-refresh-view-reference.md": { "sourceHash": "sha256:5ae489e72f396630aec7b81960492180ce2359df98a403503cc7613fd2f22049", + "sourceBlob": "c85ccc03ce66f1e851186a77121fcbb8a7e99900", "status": "translated" }, "getting-started/views/diagram-view-reference.md": { "sourceHash": "sha256:dfc344df0e16e8b8e186954e9cee6883ce21b968351b5d590947c24c9219617e", + "sourceBlob": "72755e9142cd6ea7a41571b151b44af7a66b4e85", "status": "translated" }, "getting-started/views/find-replace-reference.md": { "sourceHash": "sha256:a207ceafc5c8e3d3a218c80d405fafc3c71a9d9491964366607ee205e28a2b69", + "sourceBlob": "ba9c3fe3d81b4b88801c5ee36d1f549b785f2c60", "status": "translated" }, "getting-started/views/macros-view-reference.md": { "sourceHash": "sha256:684b3ae341bc7955f0f304e04782f7b80505ca4bb2490a7c91e99e19439b743a", + "sourceBlob": "9011cd129c99784016784243f0b4ee02a2563935", "status": "translated" }, "getting-started/views/messages-view-reference.md": { "sourceHash": "sha256:b4e921bba59577a9f8566439d26b22c2fa5e6dd7abc73ca5dee661d2d659c627", + "sourceBlob": "e329095be740023fce331f0510f7c95fe8e89bf0", "status": "translated" }, "getting-started/views/properties-view-reference.md": { "sourceHash": "sha256:6915dedc081501314e88a8e8700a946ec5217564c2b12e8aa45b3b4a54ad2a81", + "sourceBlob": "432f1db8f3f038c3b0d981ae84dad1717094ae1e", "status": "translated" }, "getting-started/views/table-preview-reference.md": { "sourceHash": "sha256:f010ef6c700738379e6cbf20325a39c7b86eafaa5728d59bd4adfd27b6db8dae", + "sourceBlob": "4a72949ba4c0d0afa312a7f732d48698f991d30c", "status": "translated" }, "getting-started/views/tom-explorer-view-reference.md": { "sourceHash": "sha256:e9d173f9dbc158abac089ab057372a1ef392b05a86c538c9e1dc931a0ff9f863", + "sourceBlob": "55e085d4cddf2da47e9dd983fd6576aa3a7b1da8", "status": "translated" }, "getting-started/views/user-interface-reference.md": { "sourceHash": "sha256:4d45273ca50773ebbcc8687abbda271ee00eef21559ad544f7c7e1c85875aac3", + "sourceBlob": "f57a8b263b7c1479670f1cbb394b304231ccfa17", "status": "translated" }, "how-tos/Advanced-Filtering-of-the-Explorer-Tree.md": { "sourceHash": "sha256:65c40ce723d4b32aff6b4591f2bbc7b92f9efcc4d8f5fee3beeeaf4faf58cd71", + "sourceBlob": "e9693aede33659c45ebc01810371f096a79a6d4b", "status": "translated" }, "how-tos/Advanced-Scripting.md": { "sourceHash": "sha256:c840e18a28dd83bd4d3d155855210b5b7f917cfcb41e3ff54fff7b9574015f85", + "sourceBlob": "55f6b13aa957f4249b43b1978d9bb7ddec2f13b1", "status": "translated" }, "how-tos/change-compatibility-mode.md": { "sourceHash": "sha256:3fbf4c15fa1139dceed32d02cc4c59e4d46c17e8bca55704057158be01f44300", + "sourceBlob": "82a38c6beda6c8d4f5e8afcef8f2ff733471447b", "status": "translated" }, "how-tos/connect-ssas.md": { "sourceHash": "sha256:ac0be330421f0ec892ea2f79efbfe6ab6115dbce8b663be873442f6d31266a3b", + "sourceBlob": "7ff28f3b00e7b2319aa3fab30c6b5f8e8ebabe84", "status": "translated" }, "how-tos/deploy-current-model.md": { "sourceHash": "sha256:678aaf20d780c1b7489968a3ddd0dc11553f102e5db763a0d545b49e368b5301", + "sourceBlob": "94011f3b3bc0dc6199c2042cc70b9b31cb094366", "status": "translated" }, "how-tos/drag-drop.md": { "sourceHash": "sha256:56d2daf749079cedf7edccdd1a6981c5cfc49aadfd04b91a2f7a95f481c2e3b9", + "sourceBlob": "86ead6b515aa5cf4f317413aa582e47c79afffa2", "status": "translated" }, "how-tos/duplicate-batchrename.md": { "sourceHash": "sha256:c0595217b896e03211f74b7fe747e6f6d9ca610669b0e08fe07ff459e2b39d20", + "sourceBlob": "ba58dd3f3f53b5bde0876bbd4a20833000bc5f0f", "status": "translated" }, "how-tos/edit-properties.md": { "sourceHash": "sha256:36d9f419afff9ce4d1ba814f1a97ec3c825e569dc0698c82d6ccec72380cbabd", + "sourceBlob": "84734a92e2f9f839920b9c909dc9ffc36ad46e98", "status": "translated" }, "how-tos/folder-serialization.md": { "sourceHash": "sha256:85959de08723f8acb20dc4f76220833d0d9fa9f71724fe1e68673c53041c0294", + "sourceBlob": "8a08bf0ee4a1a12987ff8f9bf646bf2e1e1952ab", "status": "translated" }, "how-tos/formula-fixup-dependencies.md": { "sourceHash": "sha256:f1b335d4a4c069ac21bdd9c20b0e34ed6442c63307f9d3a1cfdb29e4992daec5", + "sourceBlob": "b5231255f91b1d601fe3dde75e8334f3f243fb0e", "status": "translated" }, "how-tos/import-export-translations.md": { "sourceHash": "sha256:e360d9f6fb69f4ed660b150bc7cdbeaad188998f9addaca07a1ba95880158fbc", + "sourceBlob": "cf804376de4ed6cdbb410c91ad54adeb9ebab838", "status": "translated" }, "how-tos/importing-tables-from-excel.md": { "sourceHash": "sha256:6a68727673ad603d2091784ea68b0fd13ebc1eed7c5e279366841237e3319e9a", + "sourceBlob": "964498c7544e700c1a20e38f413c22a26c6a7d83", "status": "translated" }, "how-tos/Importing-Tables.md": { "sourceHash": "sha256:a4ec186a6a0cd94f851af6dc8a7277cad1216b3b63a0d973a7604774abe94f00", + "sourceBlob": "15052fae44f4edb632f91662e2ab026e5de5afe0", "status": "translated" }, "how-tos/includes/sample-metricview.md": { "sourceHash": "sha256:fccd99fc0bee912d39f879757f5b18d263bbe88d130d534fe5663283a2351dad", + "sourceBlob": "883819be7e347081b45543d64467304593e62fcd", "status": "translated" }, "how-tos/incremental-refresh2-h.md": { "sourceHash": "sha256:891d5b8bfa767c0bcfead8fa290e4f5d79116f9c95377fe1c6af51bcc56058f3", + "sourceBlob": "d0a1a2f6ff66142d05f782e5dfa959357c4556a9", "status": "translated" }, "how-tos/index.md": { "sourceHash": "sha256:7e51a733575206571c23bf168421d79bfc414e12300e2a04e23c98d5d1710a45", + "sourceBlob": "6202ed72237521668d52857ec6a7db2bfd038f65", "status": "translated" }, "how-tos/load-save.md": { "sourceHash": "sha256:4ac29ac34e1aee8edd2733d947e4519b26dbdef29ba4a372362a644d0488b1c9", + "sourceBlob": "4fa5c3c688bab68fddc3911549d7f0af864511e1", "status": "translated" }, "how-tos/Master-model-pattern.md": { "sourceHash": "sha256:b31a867bfe995adbb8ee02bdf5ff86f861752c6c98458f05b4b86db0980706a1", + "sourceBlob": "75ff116a81458138e84b66dcdcde2da7186bce43", "status": "translated" }, "how-tos/metadata-backup.md": { "sourceHash": "sha256:c61ea5c6dcce01019831e07daea29cee37a02e0dd5a05da7e71c6f42176893f8", + "sourceBlob": "035f1fb0cbc1cf94253b1a1e2e3cec50c4bc6bb9", "status": "translated" }, "how-tos/perspectives-translations.md": { "sourceHash": "sha256:65165835a80d24a358493e2af433e6d3833c039846e65fe9977c5d279d0f35f5", + "sourceBlob": "87113757d34a486c0a4e7d51004a110e50a499d4", "status": "translated" }, "how-tos/powerbi-xmla-pbix-workaround.md": { "sourceHash": "sha256:6ea66fe373bb9569fccaa28054c7841f07ffd88cd37196005e7a4fe2d78c4607", + "sourceBlob": "af4beb618c060fa28c0c3bdd3226ca732e716a6a", "status": "translated" }, "how-tos/replace-tables.md": { "sourceHash": "sha256:9e243b780ad11b7ed0d7bd28f34b7303a1de04f5c878123852e48aa69d5ffbf1", + "sourceBlob": "d3901ed42e65e427c92e58487f5354b3ae3476b7", "status": "translated" }, "how-tos/roles-rls.md": { "sourceHash": "sha256:252723e16bdc1e9eb192e6a7360e074ed8055c8ffa418d17b769b67b2cbb125d", + "sourceBlob": "393cfaa6cb11ecbe45faf578d8ff8d5cb5b9c328", "status": "translated" }, "how-tos/script-reference-objects.md": { "sourceHash": "sha256:f4807e41f2785b359bc6b9875519aaebdca84da06415bea5a6cd09179a59e18e", + "sourceBlob": "433ae78ee0d41f19ea256467943f17ba73976399", "status": "translated" }, "how-tos/scripting-add-clone-remove-objects.md": { "sourceHash": "sha256:4d28cbac2e46174af4ed28927c660275192fb013621c5b2d37c0dd5563f95f07", + "sourceBlob": "d5cfc7faafc9594c64b2ed690a8a6a99368ed8ee", "status": "translated" }, "how-tos/scripting-check-object-types.md": { "sourceHash": "sha256:e5fb737f603fb9234e8177ce7d888e6f73219b3823561bfea0d2db110e9788c0", + "sourceBlob": "c6188ad2a4fd831a36f46a90bbe5a0db795b4404", "status": "translated" }, "how-tos/scripting-custom-winforms-dialogs.md": { "sourceHash": "sha256:9830c8cea603b15203226db2df1e217209cc5b78e67c14fbbe11f9cbc1f229e5", + "sourceBlob": "805245219fd1e9eaadd2cf2a8a3fc1f6492def3d", "status": "translated" }, "how-tos/scripting-dynamic-linq-vs-csharp.md": { "sourceHash": "sha256:e989800b165f6c5b86076bbdb28e749cd7e882f7be7ee0f72f9e3924634abbe9", + "sourceBlob": "60bb8da7e18d3701169c1dd2881e061a6d9ea570", "status": "translated" }, "how-tos/scripting-filter-query-linq.md": { "sourceHash": "sha256:a269be822be81370fd10a982730d6eb1f47bc3ed2cc6081a28fd7b4428c632fc", + "sourceBlob": "d47a4170a72b9f4fc6bdcbd79738021c7689b1f8", "status": "translated" }, "how-tos/scripting-navigate-tom-hierarchy.md": { "sourceHash": "sha256:f12fd751688eab6151b40579ac6e2840657d4f8693364facbef5bcea131e9f8f", + "sourceBlob": "e614d0acf3fd92f519e2034521510c22c0532e8b", "status": "translated" }, "how-tos/scripting-perspectives-translations.md": { "sourceHash": "sha256:0767acd259fb03acba4804f0ce3f4f31d6fd03fb5313b000bcd7b292835ded09", + "sourceBlob": "3abafc41403b22cd846e2bd102ee389041d1f06e", "status": "translated" }, "how-tos/scripting-tom-interfaces.md": { "sourceHash": "sha256:73b571f60ab78fc67b7523f78e157a21127856a6570e53a9a101f344d2117f3c", + "sourceBlob": "ec584a1c307828a28e45d6e31475af68038ede89", "status": "translated" }, "how-tos/scripting-ui-helpers.md": { "sourceHash": "sha256:23b920a830d6bef0afa9fa6041fa3453824b50057121e5f4a6c2e92470fd00e9", + "sourceBlob": "4baf6400520710b715c0c1681b88b9b764a26b11", "status": "translated" }, "how-tos/scripting-use-selected-object.md": { "sourceHash": "sha256:430cfe77c38d7718c3908af2f7757e19b10fe2c1d5191c9113bc1c4c85fd286a", + "sourceBlob": "2b29d7f2bade0e62c41c8179b28cdb4854c8dcac", "status": "translated" }, "how-tos/scripting-work-with-annotations.md": { "sourceHash": "sha256:31f333c1fa02b5aa1b844b5e6b391f56f13b59fb4d3b6c0c4ef55d5a9a49673d", + "sourceBlob": "41280266752bb684f54960988e984871a4bdc929", "status": "translated" }, "how-tos/scripting-work-with-dependencies.md": { "sourceHash": "sha256:7d89dd667d39a7c6c397c6900e29897c28b1a8b2fdfcd815319928c1b1e79566", + "sourceBlob": "450322f96f5e3bb2f2682fb3d2ce182dfb44bc80", "status": "translated" }, "how-tos/scripting-work-with-expressions.md": { "sourceHash": "sha256:a294b1cad950289516b26454aa616a985abb037bff7f767fb17bb4ef56c77a3d", + "sourceBlob": "f0182324a9cba6c10727771deba327160e893c16", "status": "translated" }, "how-tos/semantic-bridge-add-object.md": { "sourceHash": "sha256:fc45b9109e248917012968eaed3d31ab2731c749be09ea3a3604317771dd6761", + "sourceBlob": "5853289794c1248e47cbd9436cf058a2fa7318ae", "status": "translated" }, "how-tos/semantic-bridge-how-tos.md": { "sourceHash": "sha256:3e7254cf0f1574d7a0b75109d152b36e4911d1ff417a4691b1bcafc115fd175f", + "sourceBlob": "dddea8d2f8d97917f10cc82789eba4912e00d7d7", "status": "translated" }, "how-tos/semantic-bridge-import.md": { "sourceHash": "sha256:5e44456f341235b172a178e01ac6c0ecfee7d3b6a4e4d955ba240a2ee4d0cb47", + "sourceBlob": "89dd79eccd4e906b2e55b9cbad4e08321db7d39f", "status": "translated" }, "how-tos/semantic-bridge-load-inspect.md": { "sourceHash": "sha256:5267a8486231514b4500cae234bf91aad00ecd2ccb5ae44389f46fb01b6a3de3", + "sourceBlob": "3be7731cef10e8d6b14c79b065acb008f37f9b94", "status": "translated" }, "how-tos/semantic-bridge-metric-view-handle-failures.md": { "sourceHash": "sha256:67ad4305aa673c9d258a13b33b0cfb7e00d23a22ab410f4abf197d80676a7a3b", + "sourceBlob": "b2b09c89151a48aa879766473e767e8d275fa9b5", "status": "translated" }, "how-tos/semantic-bridge-metric-view-import-from-file.md": { "sourceHash": "sha256:93070e6a840581a2c023da6990574598fba958968ef7c0c7fab22b489ae793e4", + "sourceBlob": "959f065d2b6233804002e1832992992588b3a01b", "status": "translated" }, "how-tos/semantic-bridge-remove-object.md": { "sourceHash": "sha256:4d3b3a064617e5b8792d448d6bcc6bfca71e449c7eb168e8958466096fae4fb9", + "sourceBlob": "1b936b008d16521e46b15e73b9665b3ac8e6ff89", "status": "translated" }, "how-tos/semantic-bridge-rename-objects.md": { "sourceHash": "sha256:888e6e749bc1d666eb315f3186540a12184bc538bfd73ebf97dbf68645c6092e", + "sourceBlob": "8d9a7f0614cca2bc9deb9f7d15040fd9b5ac7243", "status": "translated" }, "how-tos/semantic-bridge-serialize.md": { "sourceHash": "sha256:27ca90dfaf274d3d7933c2bb5839e2f6f28d213692e91b9e96d0b0199ac3174f", + "sourceBlob": "ac7f75806006d35a11fe820823ea3c908f23e61b", "status": "translated" }, "how-tos/semantic-bridge-validate-contextual-rules.md": { "sourceHash": "sha256:d29e2a536f0c2211f2f2a81896b281b994922d6ed8d82d6aa218fe2432c9b3e0", + "sourceBlob": "300d149c870455446d89f1a294f3889e69337624", "status": "translated" }, "how-tos/semantic-bridge-validate-default.md": { "sourceHash": "sha256:c580f957c8083de4a78ffd1b6c7c97801c7c39821d1362205f014f0d075e11cf", + "sourceBlob": "7739e9d5556f4f1270df2248041cbce5fa7a50e8", "status": "translated" }, "how-tos/semantic-bridge-validate-simple-rules.md": { "sourceHash": "sha256:8022c0d65d92617604199afbf23383d9a1f8ab74184565435f0f57b0d6cbd46e", + "sourceBlob": "18d275d46dfd61608e33c2d02367803da333d34c", "status": "translated" }, "how-tos/toc.md": { @@ -796,734 +991,917 @@ }, "how-tos/undo-redo.md": { "sourceHash": "sha256:857971043360cef22eb83ee113a0ebbe86160e1250ecfce378e7824e924fcc2d", + "sourceBlob": "a8f727796c265e8b4d78c6db70b0ce8cf14f858c", "status": "translated" }, "how-tos/update-compatibility-level.md": { "sourceHash": "sha256:ddde480e65570e65f073ce50cb6f366267f17185b5d7616b4392c5b2617c9808", + "sourceBlob": "7d0938f563bad097c57ca5c06dc9335db10f843b", "status": "translated" }, "how-tos/xmla-as-connectivity.md": { "sourceHash": "sha256:aa83da55214090f28678410499f5c8538315cab4035e722df4503c93b2bbf115", + "sourceBlob": "788ba0078f9be1ab9a58a19342277f6f903fee90", "status": "translated" }, "includes/ai-assisted.partial.md": { "sourceHash": "sha256:719add66234aa9744c77cf681f3f6dbc7d9c6d9eba02b4eede6698fd1915b2f4", + "sourceBlob": "9ceb370a56d4571df52acd9d014fd76df2f3609b", "status": "translated" }, "includes/feature-comparison.partial.md": { "sourceHash": "sha256:5eedcfa0c443e235ce6019a4301dff79f13d3d8df060985c702e1ea1807af6d7", + "sourceBlob": "ab3cde69a816175d12b841b20255ff4604dad348", "status": "translated" }, "index.md": { "sourceHash": "sha256:1418d8c61bcf18dd61bb8e62669fb07c7dee386efb23e817c1e27b923c173439", + "sourceBlob": "01783886195694d24057bf51a12805ba3d2d0784", "status": "translated" }, "kb/bpa-avoid-invalid-characters-descriptions.md": { "sourceHash": "sha256:348a0b21b9c03d6e496e239a00cfdc81d39610af0fd5ad6de6fe1ff623f7ceff", + "sourceBlob": "abd0bd25a0c7ab8cf3c87bcffa7352d2812d0e00", "status": "translated" }, "kb/bpa-avoid-invalid-characters-names.md": { "sourceHash": "sha256:8f013bf4faea84f9a24756e185ded303c9807c17b4ac5eb76df448b810e07eb0", + "sourceBlob": "be43497c2908b19810b925678e9b5f15ae8e6d97", "status": "translated" }, "kb/bpa-avoid-provider-partitions-structured.md": { "sourceHash": "sha256:86ec65070d81d5063e4d173d7c8c0966853f9b6694bbd8e726b468e7cb8b01f5", + "sourceBlob": "c0f3a440cee37381b8e7ab7e91dfefe61fffd768", "status": "translated" }, "kb/bpa-calculation-groups-no-items.md": { "sourceHash": "sha256:744c11cb3577957cc223454875f5ea95726fce0a94970698756f47b0a46c86b6", + "sourceBlob": "486293ab4b162ea7556bc08a19b913dff49f899b", "status": "translated" }, "kb/bpa-data-column-source.md": { "sourceHash": "sha256:a94e71eb040d34b57ad1f867e09460c23bf2d2e59ac848fdb92af82237851292", + "sourceBlob": "9bc9e55ba7c019f80f0640b5caf5350475144f57", "status": "translated" }, "kb/bpa-date-table-exists.md": { "sourceHash": "sha256:de3f26c0b6618516bdc57f3ea4575d35e22ff6dfe2adc856c77b6075b26842e1", + "sourceBlob": "97931246d12bdf2fea743e158eeb2dfa0297240a", "status": "translated" }, "kb/bpa-do-not-summarize-numeric.md": { "sourceHash": "sha256:244d8e116c98cab2126d5fc368471e4f432912e9e19181f58227d94b8322ddb6", + "sourceBlob": "f29ab822d626300b222279f055d74e13eebb8684", "status": "translated" }, "kb/bpa-expression-required.md": { "sourceHash": "sha256:8a411171214efbf1a8fd9da1be301ee6ca0f09bcadf38ce54082acf21984346b", + "sourceBlob": "5dd370ef5fd293e5964c5d79ffcdc21e790e7c39", "status": "translated" }, "kb/bpa-format-string-columns.md": { "sourceHash": "sha256:bf422113bd43cb69befbac033445b65b29a8569d9621a1c91eaf082a26047a9b", + "sourceBlob": "694283331456c549c1f1f5e49d83dba23f48995f", "status": "translated" }, "kb/bpa-format-string-measures.md": { "sourceHash": "sha256:bc6cd3c9bb8d66074e6f7836e4f2dd33f650df390bef17394e5cf385e744fce3", + "sourceBlob": "898dab2310a796ea3cfdd58a6bc40e683d6cb71e", "status": "translated" }, "kb/bpa-hide-foreign-keys.md": { "sourceHash": "sha256:9b67226f8b72ed7990a1c46dba893bcd36e775169fc562099bb3a8e17c47dace", + "sourceBlob": "62717d396eab4a7bee47dd8730c82c5baf2cd421", "status": "translated" }, "kb/bpa-many-to-many-single-direction.md": { "sourceHash": "sha256:b8948a747a93383b100ea910409eca99f2fc04e47360fc3e1210ba55b878d5d1", + "sourceBlob": "07454d86b7abeace0927cb1db8bf1198f5d3c1eb", "status": "translated" }, "kb/bpa-perspectives-no-objects.md": { "sourceHash": "sha256:8c2e025fa682be0443b0d926d49eef193f807af7d1df74736dcbe7d867140506", + "sourceBlob": "137725245254b98b6b61d183603a82e1c42f794f", "status": "translated" }, "kb/bpa-powerbi-latest-compatibility.md": { "sourceHash": "sha256:c4a79210717c4277ec658d7999fe9052678b865a199a22b563bdc9c21f576619", + "sourceBlob": "f05a1a043f32c3cd54cdf1dfd6c540728c0c40ed", "status": "translated" }, "kb/bpa-relationship-same-datatype.md": { "sourceHash": "sha256:291f46cc5c6ef8980aba3c960edd1e0d6d42de202f390d2ada4d4ec71342521f", + "sourceBlob": "fb3263fd4840fef07a75407f087d3f9f64913101", "status": "translated" }, "kb/bpa-remove-auto-date-table.md": { "sourceHash": "sha256:2328dbcb9c6571901c1f6565f73963c973556285e52765be67bf4d2efb3436e5", + "sourceBlob": "91451c32838492bbd5d08182d4aa8a6309db38d9", "status": "translated" }, "kb/bpa-remove-unused-data-sources.md": { "sourceHash": "sha256:58bd7d8482b7e17548d3b8f525c0f21c6648e2872815daadd8e5c1d84443f189", + "sourceBlob": "d5b82ed44dbc289e18de400acd317d125adddace", "status": "translated" }, "kb/bpa-set-isavailableinmdx-false.md": { "sourceHash": "sha256:9a5934a34ee1b4c911d855a78e87163a5427422d1ee59093b47d9c407f3bc50d", + "sourceBlob": "0ad2855c492b0a73efffd1987e3a49c52b8f1f7d", "status": "translated" }, "kb/bpa-set-isavailableinmdx-true-necessary.md": { "sourceHash": "sha256:9f1148b29e5e6ec221f24f0c4998e2da7d4a3955d23fe911dec5d63401e1094a", + "sourceBlob": "5c3062ccdc3100c2ccd3089749561a68acafe15d", "status": "translated" }, "kb/bpa-specify-application-name.md": { "sourceHash": "sha256:3de1a94d6a0b66003b65d5b06669ed905f792c3151cc36117abc73c10f52abcc", + "sourceBlob": "50dbf82bb1b645b52ff362db390f0936e03d7fba", "status": "translated" }, "kb/bpa-translate-descriptions.md": { "sourceHash": "sha256:8c73f97d93d7f0b5635b31d7e96bc5464f15f7cfc06507b8ca549edeae0cd374", + "sourceBlob": "24a01fb4145366af9c49fe2f02e50081ab03400d", "status": "translated" }, "kb/bpa-translate-display-folders.md": { "sourceHash": "sha256:10c7a4f9324e0de5c4c122ea7d10a2b6bb4e412a2f435802fc5e43d30dc65c53", + "sourceBlob": "a8b7bb3831ee01d1484896352d34d19171ed1b9d", "status": "translated" }, "kb/bpa-translate-hierarchy-levels.md": { "sourceHash": "sha256:27062bb6d7edfea8f0d78f3a789b985b18cc844690d29bc4127d55b66cac466d", + "sourceBlob": "63265dc258a5cfd66a0bd22537cb34a10566c298", "status": "translated" }, "kb/bpa-translate-perspectives.md": { "sourceHash": "sha256:8dbbdd6fd6f41cab313d51334c38cb91cdbb7830c560472a5abbdb39d40a7525", + "sourceBlob": "8a14a95448ccaa56b9e6943c1dbbca8f00594ada", "status": "translated" }, "kb/bpa-translate-visible-names.md": { "sourceHash": "sha256:48c6cc3b4998a6ca24f517a88b714f5e92c29add04010e2c607faca34c45d9da", + "sourceBlob": "db2d51f609860d881e46d51d9b762de7eb6ff3ae", "status": "translated" }, "kb/bpa-trim-object-names.md": { "sourceHash": "sha256:f9c4103682dd64f14587c68948264b3049aa3fcdb63997d7ebd3645075c4aa5a", + "sourceBlob": "ecfd33ebaf29ae5d8d69f6b4e73c111ed99631c2", "status": "translated" }, "kb/bpa-udf-use-compound-names.md": { "sourceHash": "sha256:6a626e28ba35b857383cbaf6069708880026531ad2dba4faee0a09cb52bb102e", + "sourceBlob": "062f09146b0fd25d65271d4695110f8e5a711b00", "status": "translated" }, "kb/bpa-visible-objects-no-description.md": { "sourceHash": "sha256:be6aff3d1618ad140b7be83ae59f97756572de2bc8b84eb5e035953f39642abb", + "sourceBlob": "975aaa1668ecf93ec148c560cd11e0bba47e3a16", "status": "translated" }, "kb/DI001.md": { "sourceHash": "sha256:0fdec75eb29be6ad908093cf6a95b9ab331bb568382772bf814fcc919a2dfc2a", + "sourceBlob": "d14d422967fbca7f07c93160b418e7e9ce09ca3f", "status": "translated" }, "kb/DI002.md": { "sourceHash": "sha256:068a81baf4c2c490ef06e51cfb9886723242dce676a9edea26e0948c0b00f702", + "sourceBlob": "7ef4489b2398ef807c81418dcead556913dd9fe0", "status": "translated" }, "kb/DI003.md": { "sourceHash": "sha256:b8e00a8116d08be012100d264b75fec171eedfc043a1bc52b1f2cf074c2dd0e1", + "sourceBlob": "5e334e1a8ab5dc88af4ac9e231d9317a09fdd8ec", "status": "translated" }, "kb/DI004.md": { "sourceHash": "sha256:0252909c58292e84696c2df9ce0ffb93452b96a823a684b06864f7e1d4b8fcec", + "sourceBlob": "c277438258ffa2d4288410951b88fd5a5a9c193b", "status": "translated" }, "kb/DI005.md": { "sourceHash": "sha256:8e03cd78771896db6b9dfb81a57e0a45f42335d6b242e0d474699da70843de53", + "sourceBlob": "a824b872fe91c257835ac593a6630bd0aae043e6", "status": "translated" }, "kb/DI006.md": { "sourceHash": "sha256:96086b12841c4fe8365a83fc91fb167c81d915b2c98e448489bdf55773167704", + "sourceBlob": "c211ed688d02976080723fd23e86a8b19171d3ad", "status": "translated" }, "kb/DI007.md": { "sourceHash": "sha256:78dd569b532ba7d5cb0efe9c96e5623604c5c9dadd67949d65ad14449832c1b7", + "sourceBlob": "e77da953e7178ebfc69524bc06db0afb6efe1d3b", "status": "translated" }, "kb/DI008.md": { "sourceHash": "sha256:5521d0162fdacf2da404c49d5715d5124f4fa682f793dce4df3d7b952519a2bc", + "sourceBlob": "6d9ffd07ea2419add3e5a9901f8e737916c81a35", "status": "translated" }, "kb/DI009.md": { "sourceHash": "sha256:790d74322a9d163c6acb9bf5d2b0e45352b85b954d68f2c1bddb06c4600361b3", + "sourceBlob": "7511b303c76658a947af3f720abf1f9750d282cf", "status": "translated" }, "kb/DI010.md": { "sourceHash": "sha256:bf5c57cf42365ee7869cb9f4d24c5ca20f96f79f73007a587b9034d25bb321df", + "sourceBlob": "5a96bc2247629a912bbed0e873743fd722fbc5d1", "status": "translated" }, "kb/DI011.md": { "sourceHash": "sha256:805dd2b2e2e754ef66ab07bba08fa0755c519e7432af9e83fbf4490a49f68924", + "sourceBlob": "34e89e0d3847256197cf309fd91bcaca9407942a", "status": "translated" }, "kb/DI012.md": { "sourceHash": "sha256:b9934cee7d4ea8c47cb70de48454918e64326fb8f76bee17db773b0f32627462", + "sourceBlob": "f90382e17bc4050f8cee1dc86a230c5e58a61a0a", "status": "translated" }, "kb/DI013.md": { "sourceHash": "sha256:19a2309ca26b9eedf30cde50cb70f3f70e8d8aa786ac98fc3e7bab74143d54e6", + "sourceBlob": "bdd5357797a7b753e584fded001ad022bc7e0aeb", "status": "translated" }, "kb/DI014.md": { "sourceHash": "sha256:be9a88c83f9dfa328b63298e7adf2a809b2cd83ea03cb14886838ac891536f1c", + "sourceBlob": "7a2b64d35cd0f94a45875abc87b98cdd97a3c8d7", "status": "translated" }, "kb/DI015.md": { "sourceHash": "sha256:c1c69cade3182935031f846cbe3f4165a4fd453ca77012e76f4f8cff1de2d3c3", + "sourceBlob": "bb8909be2c4ea9cf438c9fce99cb5cd9afeb7449", "status": "translated" }, "kb/DR001.md": { "sourceHash": "sha256:ad922ebf457863f55e34000fd78fc007c0d69f9bee556d2f79b5cd27c03273fa", + "sourceBlob": "567864910b021fdfca495800c76dec356ee776e8", "status": "translated" }, "kb/DR002.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:0ad80ebc8cbf3ec27662dffc0210986cbabe8f92810bdb7a33717de8fab16037", + "sourceBlob": "edfa81ca29cd1af043dd6742a461098f183f3f6b", + "status": "translated" }, "kb/DR003.md": { "sourceHash": "sha256:b76298c84d8c1269703dba2770d51a6674dfa4f03979e5aa71a53a456575bb49", + "sourceBlob": "8e6a5cd241342f8471369a4f436280446feb5468", "status": "translated" }, "kb/DR004.md": { "sourceHash": "sha256:d370d93822297f2614b44a30c04d330e5feef34160b080eb9ad3e019c70c7f6b", + "sourceBlob": "46db6e8307fa2f884b75663696d014d1c1b0d5f2", "status": "translated" }, "kb/DR005.md": { "sourceHash": "sha256:5b1451993738cd1b1737d47e96f328e5899f14028c161fe7c92357d74bfd29da", + "sourceBlob": "bec87ff60f07821e899b674b3887485c551fde5b", "status": "translated" }, "kb/DR006.md": { "sourceHash": "sha256:a91536c340fc07b1293e3a2e8cd4e89828d9fbdd8bacf3b23a736cfb8314e329", + "sourceBlob": "1f2da5f8e072439b8c6986d3c583de308be71006", "status": "translated" }, "kb/DR007.md": { "sourceHash": "sha256:a03a20219e9ed635de09831705034fe148912a694c50d8d36b87b4efa26ffbd9", + "sourceBlob": "5ef85825e6e5f4702156e2f5b8f74675a6020116", "status": "translated" }, "kb/DR008.md": { "sourceHash": "sha256:980a8345025fe79ac9cb8c3f841b204702f60dadb9574d72b33ba229cf9a475b", + "sourceBlob": "3eb0c734bc8cce8dc91d8eadc5e2480be52a2819", "status": "translated" }, "kb/DR009.md": { "sourceHash": "sha256:8609e608aaebfce4391671248f28d02870da055a99c90e126d56c4b415ceabb9", + "sourceBlob": "d909b86812cef9717b8fb0182f1349a7b1c8d920", "status": "translated" }, "kb/DR010.md": { "sourceHash": "sha256:76c8d124d90f77650b62cc3b03402177ef55f25898b032bc7b94180130778485", + "sourceBlob": "5de81896dba1404a890b2c047c6b80d30b63cfe3", "status": "translated" }, "kb/DR011.md": { "sourceHash": "sha256:ddb675bac420ba94a6a2eef2f7ab9cb4c9981b23398cc2a984a5cd30197743d2", + "sourceBlob": "9906b237f84815fc30ce12ce46e9658243eb510d", "status": "translated" }, "kb/DR012.md": { "sourceHash": "sha256:dc8c217235ef82b45ca02328f0e1d78ccc12114dddd5a44394126ec3cd638a61", + "sourceBlob": "28e69bf7eaeff3de9ab07e8a87481cc695eb391b", "status": "translated" }, "kb/DR013.md": { "sourceHash": "sha256:3e81af96332221c3b1f36655a6ee3f8dc0b116c019cdb7a5c26c5971c9cfc51f", + "sourceBlob": "7f3ef591449a24f94390f77301246fb1205f6102", "status": "translated" }, "kb/DR014.md": { "sourceHash": "sha256:d435c5959eb47b00a7aab3b846013228ec6d2077a3dc245e50c1a5328c392dbc", + "sourceBlob": "eea06f39e674a5efcb913329fff66538e282ac1c", "status": "translated" }, "kb/index.md": { "sourceHash": "sha256:8331987f38c8b52d8907a3f0d2c773d188e888cc823f2a5a21463d0a163f7a15", + "sourceBlob": "7306c1be29588cdb8e38fe55db3d5889dc7f0eb1", "status": "translated" }, "kb/RW001.md": { "sourceHash": "sha256:cc6cd9a2294f425380d825ff0a5ca882c3bd40a4d5207cf6cce0793f7c6af493", + "sourceBlob": "c08d64e9d73fdc1ca299b9b2e82e74238bea6baf", "status": "translated" }, "kb/RW002.md": { "sourceHash": "sha256:597595d6f8a1102114f51ae992bb519d21095f2a1003a8b1a90d3d96849916c2", + "sourceBlob": "f2e2d6b94caa5f166bf99fab7921d862b5b9e52e", "status": "translated" }, "kb/RW003.md": { "sourceHash": "sha256:289a445b62c13098e4ed6af7fa37901b1aec8bfb4d3b57e1e42eda8f1de9ad5e", + "sourceBlob": "c6cdbb64497acfd506e74ba67e4e56842a042b6f", "status": "translated" }, "kb/toc.md": { "sourceHash": "sha256:db0f8007f8bb5998ac188559ffcd9831d49a8c938f1b41434295625097a0a160", + "sourceBlob": "dd2c1e8d930ecabc9af854dd1b328d363be34697", "status": "translated" }, "references/accessibility.md": { "sourceHash": "sha256:2bca6bf296f3d236dfb29374577829002bf7dc9b42f6d5d29b8bb1e52be1691c", + "sourceBlob": "098c02fa53b19cb3e9ca766b8b15458bbec9b329", "status": "translated" }, "references/application-language.md": { "sourceHash": "sha256:3d8bb1e49bd7cf97c56210ab5c8322b7c419cbb0152966f195cabeabb87d1ded", + "sourceBlob": "c676538ed7467d3ba12ee644059cdb9856196175", "status": "translated" }, "references/downloads.md": { - "sourceHash": "sha256:d6f5f62ef64bfef5eab06ef77caad78ee695cd60e12114b7af9774a337063085", + "sourceHash": "sha256:27c21529bb0408933aedeb5614c99a4a2bc382554c45e4dd75e8f0fc45fa4281", + "sourceBlob": "551ccb2d3b4f9fe23823564d3f93b843bdc886ff", "status": "translated" }, "references/FAQ.md": { "sourceHash": "sha256:bf29800bdcc5ace4dd5d3813fc1fb8c48b30a034039a0285bd62768866b924e9", + "sourceBlob": "a52f303bdc0045ee8a8afa343a24e967ee09e141", "status": "translated" }, "references/FormatDax.md": { "sourceHash": "sha256:d6d07781fc5c8cbecb8187caa6123eb0a13f0f145abfa4b8c4cb1274c7e9dcf2", + "sourceBlob": "1f798e620c1b8873ff9948ce26655846070868f1", "status": "translated" }, "references/index.md": { "sourceHash": "sha256:f3f76163564f856590cabfa5c74ee31aadbdd37448f41b46bbd36358c3e60cef", + "sourceBlob": "9d07f1812f00c4ef6d531f0eb9a2467c52b265b7", "status": "translated" }, "references/Keyboard-Shortcuts2.md": { "sourceHash": "sha256:6dac63cca6fed07aeeaa4c9c97b9174104c299528f823785cb9d042fc567f93c", + "sourceBlob": "18406beb0667d006856d3e0ac2b616928730258f", "status": "translated" }, "references/object-properties/calculation-groups.md": { "sourceHash": "sha256:4f230c59f60e75119e22ffada811698effc81878640903305bfa17a506f28dc0", + "sourceBlob": "fc8fa1b2e9a6b5596e7eb4a7c5e2a91ed936465c", "status": "translated" }, "references/object-properties/calendars.md": { "sourceHash": "sha256:a649dea71fd889bb47c4778ea1b387e128dd16ba550fa99848eb12d4dbde7f03", + "sourceBlob": "22c32f600f5a47468e2451fe32644c6ac03518a7", "status": "translated" }, "references/object-properties/columns.md": { "sourceHash": "sha256:ad8d5949aeeb063589faa8dad1fc7ad32f7e5b4081901af99db80e2362654d6d", + "sourceBlob": "7f9de93c50705171f0ea1580c988c21ff4c23019", "status": "translated" }, "references/object-properties/common-properties.md": { "sourceHash": "sha256:99a20e619fbfc8aa59e91d3c78a0d8d6153b21d3c44275b0ec4edb3833a343d7", + "sourceBlob": "a85f44eef5b17a8ad1fcb3c911bd1a7d1a280b6c", "status": "translated" }, "references/object-properties/data-sources.md": { "sourceHash": "sha256:71c36ce369386fe8fbc5fc018e16fa30630605cf1e5fffac0e1d677bbb112bfe", + "sourceBlob": "df22b00cf3fa4a2da8379cb4ed75cd34575d7164", "status": "translated" }, "references/object-properties/functions.md": { "sourceHash": "sha256:16052cd3ae051a78d1b8723a2fc2cd07a675af2d4a75e4eb9adda44ebe2277ce", + "sourceBlob": "0c3c36de8e42b90be47ed1c98ff690fabf448265", "status": "translated" }, "references/object-properties/hierarchies.md": { "sourceHash": "sha256:801a8e2d3f529a5df6324a2449a2f10cb476d1d327c86b797e574f1bb767c64a", + "sourceBlob": "c17885494b4dcf220d189e16f14ea22585653083", "status": "translated" }, "references/object-properties/index.md": { "sourceHash": "sha256:f81a240dfc097835d1528695b989f4fc6f7ceb13f7820ba0510445a0ab92b8e2", + "sourceBlob": "777658c2bcd6eeac0aaf867a7068a1622f054d3c", "status": "translated" }, "references/object-properties/measures.md": { "sourceHash": "sha256:a56d044c177c1c788573e56c9c6c37d079b4b902a07ea3c2d4d1fc9a8c59c746", + "sourceBlob": "de6848a00e4ca9dd9831677207e9da83acc3900b", "status": "translated" }, "references/object-properties/model.md": { "sourceHash": "sha256:b101b856fcb7d2e9040b2fffa33d8fb712a65abbe20d37b332981031e1178bce", + "sourceBlob": "bd64cb58342640fb5998aaf98cf6fb50c5648728", "status": "translated" }, "references/object-properties/partitions.md": { "sourceHash": "sha256:77568919d2c1bb8e7ab595a3ceb5e763f07bb6720148e36e9fc45dd3b50991a1", + "sourceBlob": "99eb9dd8860b5e474841385803e43cd5e874ec3e", "status": "translated" }, "references/object-properties/perspectives-cultures.md": { "sourceHash": "sha256:ad263191d75b254fb88a0c2f3cbbf2e6eeb12f8bcdcc606c41ff059caebeca5a", + "sourceBlob": "02968a407205f76b5471f9e2ab140b43a619e5b6", "status": "translated" }, "references/object-properties/relationships.md": { "sourceHash": "sha256:99a40c5125f2607d9c5e341f2f2cd8eceebf6948da6f2bbf0c8294c49c079424", + "sourceBlob": "9fcfcd5ed6d436022bb124a7dfdc5c2f22951325", "status": "translated" }, "references/object-properties/security.md": { "sourceHash": "sha256:107036e38dcfb54408fdc842b3c188662b652bd10f8ccd43a1969caab93ab4a1", + "sourceBlob": "2b99f874ef26c9abce1b6c56533ee665e04b3e33", "status": "translated" }, "references/object-properties/tables.md": { "sourceHash": "sha256:ed8f9d50743d48e7e97e93a5b9008766228c89f8bab8fef4565f1399795f082f", + "sourceBlob": "ee2eb6cae06e4753b42282b1dc5e4aeb8cffb640", "status": "translated" }, "references/policies.md": { "sourceHash": "sha256:2caab8e0667baf27214a05cd1d8083570418378257a94a739c21abf6060ef5ac", + "sourceBlob": "3a1cb3d5d125926e4691709c8a5c7f3e6c5a70e9", "status": "translated" }, "references/preferences.md": { "sourceHash": "sha256:4b727d58249a9c4583c67d0dc40a32ebd45efe9e006f86cf404c53f094e8bcd9", + "sourceBlob": "561866a184e6faadce56d47bde51a32c274f1ec4", "status": "translated" }, "references/release-history.md": { - "sourceHash": "sha256:78226d0901a9debfe95e062f96697ba023c893852edcf6560dc89296382c0fc1", + "sourceHash": "sha256:3c28cf2db33088ef78f5af074d2c62d2678fc8a726768d62373c4e453ba16b96", + "sourceBlob": "c081f42cfb2bedc78a85dff2f7217b827b9946d9", "status": "translated" }, "references/release-notes/3_0_1.md": { "sourceHash": "sha256:9dd7d412075ffeb73fd903e6e560738125b944c1107d5279e06278b3a1c67ac6", + "sourceBlob": "eaa08f56a3be3673007e999c59ce874478c7861a", "status": "translated" }, "references/release-notes/3_0_10.md": { "sourceHash": "sha256:e4ed9e02f1d530888ea3291a800b799de6e5dce5717597aed5b02f96e6874819", + "sourceBlob": "4a09302e5a9863660c6f7ffb800ee39032ff7886", "status": "translated" }, "references/release-notes/3_0_2.md": { "sourceHash": "sha256:c259a508fc8aa8ab579248baa48edc26886cc11a7b46c1c300f5381f7f9a7576", + "sourceBlob": "640a8456627d2c06977b1b3d584ef2f8c33e4606", "status": "translated" }, "references/release-notes/3_0_3.md": { "sourceHash": "sha256:e34eb6b6cc40a550c2b1e5555ab3dd5ae9aab4fe150191da8b217f7e1c44b7f8", + "sourceBlob": "e53733eec2e5cae03cc0d3aa97ebf5a3b520a61a", "status": "translated" }, "references/release-notes/3_0_4.md": { "sourceHash": "sha256:0745acfac1154de9e0b61300052c871454382fff3120fa89b0bf5b794522bcb1", + "sourceBlob": "c5d3be45cca8e8823ee5bde6ed3cd399f12d8f68", "status": "translated" }, "references/release-notes/3_0_5.md": { "sourceHash": "sha256:0bc3323e03649c932b63cb6997eb73c33f7d8211601640a21630c7cb61851ffd", + "sourceBlob": "f6d6460df73b652bf239dfce67098268e441472e", "status": "translated" }, "references/release-notes/3_0_6.md": { "sourceHash": "sha256:1e570cc335e2d65c8c4a137009c2aedaa3aeb09e0a45140121b6bc7cbe1d7468", + "sourceBlob": "fdcd7af39cfac6d6f1a09575663b73491ed698df", "status": "translated" }, "references/release-notes/3_0_7.md": { "sourceHash": "sha256:5abefcdef1f490a553d1a625f1b3ae2c22e063ee14d751d483feeb1d347593bc", + "sourceBlob": "61e8539956e33e86ec42193f4a84e3b4caa253e8", "status": "translated" }, "references/release-notes/3_0_8.md": { "sourceHash": "sha256:cb1bfca1fe61a069fe1f3bf3f53ea69ba4209a25e434d94b0b1ff4fb63d64012", + "sourceBlob": "8ac0a6fd92d4cd375f72bee6aa2418ac7615b0e7", "status": "translated" }, "references/release-notes/3_0_9.md": { "sourceHash": "sha256:645ce98903aff7368af55a9b23fdeff5964c5804e16d3dfbbebe34f2eeaed37d", + "sourceBlob": "9826f4dd043afc8284fd6f83a323520be4ac95cc", "status": "translated" }, "references/release-notes/3_10_0.md": { "sourceHash": "sha256:280c2247e870b506a5134ad2da70ea21f53a5d7b2ad8e9f5319414e2f21d29b9", + "sourceBlob": "5ed8e34b1e58c79382f6d55d13fb01651504b6e1", "status": "translated" }, "references/release-notes/3_10_1.md": { "sourceHash": "sha256:8fcb4cee86daee9325723dc6cb6b9f3ffb3d9a41e67f0fc2c35510b8e0a78521", + "sourceBlob": "52b9d415dc4d1794ec1362fe2d6bae47a15de845", "status": "translated" }, "references/release-notes/3_11_0.md": { "sourceHash": "sha256:00487e4ef0ddf0ee9e93644ec740a1381bc984d716553cb60d2abc0140fc7b6d", + "sourceBlob": "79d8d94549a1157183b3d8946453fbc7f6878507", "status": "translated" }, "references/release-notes/3_12_0.md": { "sourceHash": "sha256:5ce41677cf81415a721c8db3c5c7e435c8b9c6fcf6ba61af7932cfcd2cd7f2b6", + "sourceBlob": "cdb6b3c2a3f6ff331a65c0f73641edd54b6e4b21", "status": "translated" }, "references/release-notes/3_12_1.md": { "sourceHash": "sha256:48c8104c99b0af6a26485ed1e19971ac9a06ca2fa143c55d1c2c5c74fe5252ca", + "sourceBlob": "be5152a46c06e8d0490a6911ec4982a0cd877eb7", "status": "translated" }, "references/release-notes/3_13_0.md": { "sourceHash": "sha256:43b9bff69d3f4fd23c2f98102be764674c6ee63c5832e8dccbf6cadcb2d5cb0a", + "sourceBlob": "258c28d26272a5841458961a7d47d2fafeb09fd4", "status": "translated" }, "references/release-notes/3_14_0.md": { "sourceHash": "sha256:63eea06882ec7b1838526b803fe22776fc3489a78e51a98acc3f4710212f5df9", + "sourceBlob": "157e5593dfd730dfd96767d6c0286cfc8ae67517", "status": "translated" }, "references/release-notes/3_15_0.md": { "sourceHash": "sha256:892511af91a5becc0cddf3c17031eb2772f1c0362f2e144fa6137a9cfef864e1", + "sourceBlob": "8efb40552718a548aa30d064a20715ffae67f825", "status": "translated" }, "references/release-notes/3_16_0.md": { "sourceHash": "sha256:c370d56ce763d64929dab9191dfec25d4d8e5cf4aac5ae242c965213497c07c9", + "sourceBlob": "3b266e6b9f7264325abfcf6dc129c2a2a801bdd2", "status": "translated" }, "references/release-notes/3_16_1.md": { "sourceHash": "sha256:8c963ee75f969d1f42991744d912d3ee9901209fe4729421791fff43cef34c16", + "sourceBlob": "beacd90074a0d632e9645d41bc34d3e6f85af898", "status": "translated" }, "references/release-notes/3_16_2.md": { "sourceHash": "sha256:2afcb4c569c945fcefcbb4ae5db451c3d3e6c386d6ce01520456af225cc397d7", + "sourceBlob": "5274249e21b216caf5da1f05c77b11ddc886aa7f", "status": "translated" }, "references/release-notes/3_17_0.md": { "sourceHash": "sha256:c8d747fa588ed2e9724ca5642094b3183a7848de106206c5acb7a6db003f03ad", + "sourceBlob": "e891fe862f50e87038435403460381e9c4275788", "status": "translated" }, "references/release-notes/3_17_1.md": { "sourceHash": "sha256:43426ca1856ba7146d873c25c365fd7ca9dd6c2dac27172d4302aa08caf73a13", + "sourceBlob": "6f2d2154cbdae6bb8b3bbdad7fa5e50c36f5cd03", "status": "translated" }, "references/release-notes/3_18_0.md": { "sourceHash": "sha256:ccef1d643cac9bbba5813f1f9101466c79a12f8adf5622f1b20780964bd39b36", + "sourceBlob": "c3672c4a50f8ab0a36e032f144a6b341b081f570", "status": "translated" }, "references/release-notes/3_18_1.md": { "sourceHash": "sha256:864335c116512c8d22394bc0b36873a8c4855d1a61a18f15524bd16afabb5869", + "sourceBlob": "2bc71e1f2460f5c30020657c7ee0e892a924116b", "status": "translated" }, "references/release-notes/3_18_2.md": { "sourceHash": "sha256:498e7482728a3b72acb8b42b8eec7caf732df56677d3b44ac0fbf5a32562ef17", + "sourceBlob": "9b95477e47178cc9d79e8465f48b606f2dd16fa8", "status": "translated" }, "references/release-notes/3_19_0.md": { "sourceHash": "sha256:1a50d8f5bbee16ffcefb119bcdc3f2ff8f0ee153f7f9848f0a4acd7383f8ac16", + "sourceBlob": "c10bbee907bbb2de24efeb97d8539df353765e0e", "status": "translated" }, "references/release-notes/3_1_0.md": { "sourceHash": "sha256:0585f4b1aad5d388d66147afa156d2e57e8f615ce5c3d3fb0830925d5393ee61", + "sourceBlob": "e68ce4c1162c5a60eae6649d8d24af01ca87b418", "status": "translated" }, "references/release-notes/3_1_1.md": { "sourceHash": "sha256:1d6e4b185d09f93a6688f14f954a766353fd3daa17ecedd6689735c2c01c5208", + "sourceBlob": "2b0eba0cec38d51b57cc59a652c0c18c84479590", "status": "translated" }, "references/release-notes/3_1_2.md": { "sourceHash": "sha256:9f7ef93ca36c28f28b2a61b62b7cf68738da2a31a73abbc67ebacd4be6b05915", + "sourceBlob": "22f5fc643cfd26345fab9093675f28d7e622d952", "status": "translated" }, "references/release-notes/3_1_3.md": { "sourceHash": "sha256:63da26ecf5029706dd4a732a7494b15ff4df4bbf3d4e1e7743f61b46d979d2ef", + "sourceBlob": "4e7945308676e1e8d5544cc425606e7682617054", "status": "translated" }, "references/release-notes/3_1_4.md": { "sourceHash": "sha256:690c2c05528da4f99adb4177a5e1ca448305c1093948d0cbb7948242961aa453", + "sourceBlob": "2f3c6c44bf305d44f19eaafb263980bfa693f1cf", "status": "translated" }, "references/release-notes/3_1_5.md": { "sourceHash": "sha256:c8984a8ab4c13021d98434ce71d4a54db9636dd3725c313de6795e2d6ad4fd88", + "sourceBlob": "c78926795d0da597a40ebe6031648e8327551efd", "status": "translated" }, "references/release-notes/3_1_6.md": { "sourceHash": "sha256:1e7e09b6f4d7b733050ee43873ffe842c7e55faba945cd9d5afce8996739f183", + "sourceBlob": "cc90b8afb55acd451b31b4c4e7f003f07d9f280f", "status": "translated" }, "references/release-notes/3_1_7.md": { "sourceHash": "sha256:cc33ce5dd956b6802c5184d84ead86749e676bddf9034b7228b0963ab1fa8279", + "sourceBlob": "6048b8c0f907deb1d015216badaaafd6c1d9ce09", "status": "translated" }, "references/release-notes/3_20_0.md": { "sourceHash": "sha256:5a259bbe3b2d0cb22234ba3fb754c0fc17f21a30ad3eea8af4090108d1103488", + "sourceBlob": "d4bbcf5383586b8a18c513795f6f746a4922493d", "status": "translated" }, "references/release-notes/3_20_1.md": { "sourceHash": "sha256:1adab99bfc877197b1138f98ae2889337b269b54951dbf8984609192eeb9f5ab", + "sourceBlob": "e3ebbc4d88731257564a1530a8f5cc8c32e3e85b", "status": "translated" }, "references/release-notes/3_21_0.md": { "sourceHash": "sha256:e989c0564fe64acbba6215cf25eacb1e79c6da59f17756ad9ea74500a059dc8e", + "sourceBlob": "f537e5ccbb04821871aa22107ce6f330e4e2c473", "status": "translated" }, "references/release-notes/3_22_0.md": { "sourceHash": "sha256:71625baf4598ce69ea8022cde2848d04b60229d8c60ad15bb9fb806540d62288", + "sourceBlob": "a60ffdea08da101e2afda4254b4af15effb05abc", "status": "translated" }, "references/release-notes/3_22_1.md": { "sourceHash": "sha256:1e3e64b4b3955f35edd2a9c0bae68ca092be3729c7cf46ee376234ef1f8432f8", + "sourceBlob": "311e34b247c68c60aa04d03ee1eadd4906d85b33", "status": "translated" }, "references/release-notes/3_23_0.md": { "sourceHash": "sha256:5dfe7273a83bb750fb67556bfaa0494f3474489e8584d8a4416f81db7638e4b6", + "sourceBlob": "b9930200f2cdc46ff68369cf4a8df41d8c967c24", "status": "translated" }, "references/release-notes/3_23_1.md": { "sourceHash": "sha256:a9693c900f2bd1ba870cbd36bbd3dae8c5b2cc07046953c0f1c62f8ab073b41a", + "sourceBlob": "416f497125bbdd7993af9c127b4391a17cbfeb4a", "status": "translated" }, "references/release-notes/3_24_0.md": { "sourceHash": "sha256:7e06a6582ce450f85e58773fa486f8dfa2b57ae5ff8ba5228e90a2df42b2b0a2", + "sourceBlob": "694dd28542b426a34078f0ebb7bc97cacd16fc49", "status": "translated" }, "references/release-notes/3_24_1.md": { "sourceHash": "sha256:2f6edfbfe6bac0cf0c6e0ca0c941208626b1ae2df232c8dc14046a2c6312fd9a", + "sourceBlob": "d80c0787b330d18c3b56d8dc90a1d890dc225206", "status": "translated" }, "references/release-notes/3_24_2.md": { "sourceHash": "sha256:002bd955620438ac8a76221d201bf17027262f15b5356fd994a340dae2953c46", + "sourceBlob": "cd937eed1367167e0f9719edf02728a0972e2144", "status": "translated" }, "references/release-notes/3_25_0.md": { "sourceHash": "sha256:94073554f6e49579ce01eeb1ad53b54d66d8a2949d9fb2281123688b597594b1", + "sourceBlob": "da844f0a8b78dae1af06c1e44eca177bb7073b88", "status": "translated" }, "references/release-notes/3_25_1.md": { "sourceHash": "sha256:5327769633fab3d45273026fa50388e6cc572bc4e3ad4e1685ae544d777d96be", + "sourceBlob": "40ba9e52b8d1b93c4013f9eb37f28debe2c7e1ca", "status": "translated" }, "references/release-notes/3_25_2.md": { "sourceHash": "sha256:ed0cfa9bb38b492a133d7742ffb1a57169616ea1f604ef3091d0dba292a5d9c5", + "sourceBlob": "ccad60192197d0e3ec62815a58c1600d5c4e70d4", "status": "translated" }, "references/release-notes/3_25_3.md": { "sourceHash": "sha256:e2aedc2e010c287c6a6480f950eb5abaa06c24faeec99b6f298ca9236e8ecc11", + "sourceBlob": "ec4113652b784b9140527d2ab367da6d817b49a7", "status": "translated" }, "references/release-notes/3_25_5.md": { "sourceHash": "sha256:cbceecff8d36b2bb9f4ba3a54c9957fd081f06ec869f2d37eaa14bd731854744", + "sourceBlob": "226f19b5290f99a8ff8655dc203246a8937706af", "status": "translated" }, "references/release-notes/3_26_0.md": { "sourceHash": "sha256:877aaef72b1b712c4ac6060a0812b8f6a37e85b5d7f7c907d73aaf0aec50b4c0", + "sourceBlob": "fa3c637eb355e008f8f7d9912f8de2561524de6e", "status": "translated" }, "references/release-notes/3_26_1.md": { "sourceHash": "sha256:454c83599c5098dc644a438bdf15a80714a45f53fb63ac17d8514b1b197050c8", + "sourceBlob": "65858fc9481c6ba565ccfe93caacf7ffe7ccceab", "status": "translated" }, "references/release-notes/3_26_2.md": { "sourceHash": "sha256:fa204a34a64c2f6a9e2764031e5c0c255b684edd6cbe18fd195a0d4fea2978bd", + "sourceBlob": "2930da424bbf6971360b78779a1eb08f110615ad", "status": "translated" }, "references/release-notes/3_26_3.md": { "sourceHash": "sha256:feb01ccd22d33c9adb705a0b7f6ef3523f4dfbdf1905410809d6b3f193515335", + "sourceBlob": "dbb47faa35f8d709fad409c4e74872e84b2d601a", "status": "translated" }, "references/release-notes/3_27_0_0.md": { "sourceHash": "sha256:0871275249d3bbd3ca332855873c89ed84a476929a2216333724fd61a05bdfc3", + "sourceBlob": "68e25c54d913b88c1582ce16441a2ada3f2fdf1c", "status": "translated" }, "references/release-notes/3_2_0.md": { "sourceHash": "sha256:f80a34035dde22a0619af4ce18d51f59866d2eff27418c893e676f9da877b10c", + "sourceBlob": "a41c0dd3aaa4a476f51d438c306923247438202f", "status": "translated" }, "references/release-notes/3_2_1.md": { "sourceHash": "sha256:43e1be32e10f7a17a7d86df369eef21a78ebe5b2a52b53a1f83b6fd386b29049", + "sourceBlob": "c2517496a26e4b6dc7c78b2badbb770be10cdb03", "status": "translated" }, "references/release-notes/3_2_2.md": { "sourceHash": "sha256:2da5802638de5f4ebb2f12cffb33be191351e460ce411293e865bf8310ea9747", + "sourceBlob": "62caaf92592bee009ac8060d70ccbdced3d09cd6", "status": "translated" }, "references/release-notes/3_2_3.md": { "sourceHash": "sha256:233ad017abfdf9b6cf5938bcd58d76dfac5aaa83c691e6b8613e8501ead44f6e", + "sourceBlob": "959af24862725fb1a013f758fd5f2ba904a5cd43", "status": "translated" }, "references/release-notes/3_3_0.md": { "sourceHash": "sha256:6407130a08fc4aba3742cdc01f6fcdbe315036324a6ef4ef485750bbab90d75e", + "sourceBlob": "39e8bca64ac28021e204576aafacc224d3ec1a17", "status": "translated" }, "references/release-notes/3_3_1.md": { "sourceHash": "sha256:069220239a8a82d1f0d310a468b3af4959525d5b03e11298fb2049e0ebacc52a", + "sourceBlob": "c3aa53c9e8ddecbbcf98afa66cd6b91ec3c07ccc", "status": "translated" }, "references/release-notes/3_3_2.md": { "sourceHash": "sha256:e11afa6e505c7d0d7a2cc19b12232f05b12a402d1534d689a3987a692ad6eabd", + "sourceBlob": "e629c7a0a7a9bea0c8e21dd5bcd359e35b6d50fc", "status": "translated" }, "references/release-notes/3_3_3.md": { "sourceHash": "sha256:b25a3e45ea9245ff2348aef322932081aa9419469de741bd98ab45435fc01106", + "sourceBlob": "18c520088587fa1a866707d98898cb9d84cd40bc", "status": "translated" }, "references/release-notes/3_3_4.md": { "sourceHash": "sha256:dabac3bc52ea3ecb59ffc39e10a13d252a29530412a604c284cf33fa988e7ccb", + "sourceBlob": "44b1704c61493dbce2d4bc1ab2dc690fcc6f9282", "status": "translated" }, "references/release-notes/3_3_5.md": { "sourceHash": "sha256:b88d6bc31375caca6ccbc5af1f95d1c64a64bac5d5861d540a5a111046ec2709", + "sourceBlob": "53e8a962a21b59d14df3ae2f160fdfb2aca78f72", "status": "translated" }, "references/release-notes/3_3_6.md": { "sourceHash": "sha256:2eea5eec39317d1fc007cffd6971bf08411e04b79984bac9e9e7c5f4a9237329", + "sourceBlob": "00dd2df7edb17e10f6ed795ff534113f1c4f12e2", "status": "translated" }, "references/release-notes/3_4_0.md": { "sourceHash": "sha256:45921e73217c3fd5d69b614e10428aa428b0d466bdb49d83058cb79a49b846c7", + "sourceBlob": "2199967ef076f80633a7fe3eeed70b2087c0ded2", "status": "translated" }, "references/release-notes/3_4_1.md": { "sourceHash": "sha256:2c4222b3083566a37505c7ede112ee3076111acf89a9cacb7335241c5195a1be", + "sourceBlob": "3fccb82edbf6e990dd82a79bedd88d1dbadc5c20", "status": "translated" }, "references/release-notes/3_4_2.md": { "sourceHash": "sha256:8aca2f841be6c495daf791f1c8a812605c4215639dd6d61bf9a99a1450913eb3", + "sourceBlob": "9968216781396ba6765dd9d0bed794e7890e1246", "status": "translated" }, "references/release-notes/3_5_0.md": { "sourceHash": "sha256:3e8574e571c37b8fdf644f42377a8f20e4494f57683aa2aeeb66f6cc5dbb3b3d", + "sourceBlob": "881c8d306d8e72265e29483ee7c1d2af573d3eb4", "status": "translated" }, "references/release-notes/3_5_1.md": { "sourceHash": "sha256:bbf6c0a5eaf0581ef564c23861341ba086a336a7a0c8556b18c82a3b97263de0", + "sourceBlob": "6d9a4869705ced67dfbf0b772934ef63f0f3ce04", "status": "translated" }, "references/release-notes/3_6_0.md": { "sourceHash": "sha256:390ff39fcddb43462ce6371e434faeffaec2542bf1ce3871bf109c31afbebb8e", + "sourceBlob": "d9dae63614340c0f7646dd950e14bb4461886ad6", "status": "translated" }, "references/release-notes/3_7_0.md": { "sourceHash": "sha256:1193e309b3e6b7c5ab312cdfac506a029c5d3fe1a46e55c44cfa73ac02b05559", + "sourceBlob": "a398780ba9c12f6fdc6530452736d12834c5b6fe", "status": "translated" }, "references/release-notes/3_7_1.md": { "sourceHash": "sha256:bb30b3f2c2edfb66b5b948a092b63522ffae1661d74837bec19ab8df42931536", + "sourceBlob": "d16329f471e3337ac0299c5b5459a52eefefbcaf", "status": "translated" }, "references/release-notes/3_8_0.md": { "sourceHash": "sha256:59c7b7c50555df06c3b4d8862429c9dac365f71abe0b2867433c4eb467ff227a", + "sourceBlob": "a1a261976822443a76a2ddac1c1407c9ef336916", "status": "translated" }, "references/release-notes/3_9_0.md": { "sourceHash": "sha256:435580f98c2577516a67e1c12f4c451c0f3116bc864b4dc1f474b47650d048c1", + "sourceBlob": "abc613201bffe9a8516062396010f5e4bc93d91b", "status": "translated" }, "references/release-notes/beta-16_6.md": { "sourceHash": "sha256:cb20f9fb04a9bd84b4354ccda93c724199827bd1bed1afde6704ee1c3515ad3a", + "sourceBlob": "5d83f9615f5671ccc033e54def9f496824f7e89b", "status": "translated" }, "references/release-notes/beta-17_4.md": { "sourceHash": "sha256:4932a0f3a3c1fa9f5691ff457063f111c3c44c7aab43709d18118d5e2214ad1d", + "sourceBlob": "84166e224a046502b9156d8fd7432da52ca37969", "status": "translated" }, "references/release-notes/beta-18_1.md": { "sourceHash": "sha256:948893b0e25d9c457f7757c8c2aed0be34f9fd7897e47dfbb9d48adf34281a9c", + "sourceBlob": "60f0d87cdf4bcf21a86bbc3bed6b335a208752e9", "status": "translated" }, "references/release-notes/beta-18_2.md": { "sourceHash": "sha256:44023c249d6312fe7174132138a20bee776cbaac990f9f63be3609f405cf7ecd", + "sourceBlob": "bbb976acab63bafeb87f8223519c3f7cbc45d027", "status": "translated" }, "references/release-notes/beta-18_3.md": { "sourceHash": "sha256:38d2a3684c5f3f64f6e3ae40109694ce34f5f45c7fa7105e5f4c627377faa3dd", + "sourceBlob": "964dad71690f090a70f2654078c6db72849024b0", "status": "translated" }, "references/release-notes/beta-18_4.md": { "sourceHash": "sha256:f4ab4735ebd2b80d7d6539b8f18065c5588753343112677f54e687d057bbb06b", + "sourceBlob": "c46b3385bec532838592f34578f2c176af2176c6", "status": "translated" }, "references/release-notes/beta-18_5.md": { "sourceHash": "sha256:14d1a1cc25b99f4ecbdfa2e51bd73dae43a6e3dd6536909cb6c7ca610cc84938", + "sourceBlob": "9a4cd435e8034c82e0e96aa493c3640811bc0336", "status": "translated" }, "references/roadmap.md": { "sourceHash": "sha256:b0995af14ffeba3c104b4bcc0d65bfedea42ef4e3ddeeed9e8d2d1f594efc0ef", + "sourceBlob": "30feed902ef620fa0946591f63b1da3f6656335b", "status": "translated" }, "references/Roadmap2-h.md": { "sourceHash": "sha256:91b4c2b6a63a4da62e8cd187dd8fa50e9ed7f8e4344f3a7a509dab9c228850ee", + "sourceBlob": "efd79f7499c27eb979d26e8ce014dd21391f61b2", "status": "translated" }, "references/shortcuts3.md": { "sourceHash": "sha256:01a0120df7bf52cc07963c89f687b30e92c8db68d0c7e529fb00d1ff352c84fc", + "sourceBlob": "8f3b3e1887a6e44f18593720504d454d46265c47", "status": "translated" }, "references/SQL-Server-2017-support-h.md": { "sourceHash": "sha256:91cbe97b5d1fcf552a19c198232c12a472abecf0d5ff75eb7b75b737e00598dd", + "sourceBlob": "b328f1840a4813301d1106fc411185bc3f1f080d", "status": "translated" }, "references/supported-files.md": { "sourceHash": "sha256:b914a46e482200adc4b407c54a40cdc4312d5698ff765fc661d83c4b0333b60a", + "sourceBlob": "384658d9b7607b8ea51de37c08f9e4db022514a3", "status": "translated" }, "references/system-requirements.md": { "sourceHash": "sha256:1ca5de3bd3440c895a05b276f9bf93d2a5fe80092b93928712c99994712827d1", + "sourceBlob": "97e7d12c6dc64528f758362a7c052197abc0aac9", "status": "translated" }, "references/TabularEditor.TOMWrapper-h.md": { "sourceHash": "sha256:d1ed449a9a96c0031fe8b280f1c808858196806f71363d8fa9447e0db6e7d71a", + "sourceBlob": "d394d08152bc4c7c5cb5d67b7c2f2863d0830a8d", "status": "translated" }, "references/toc.md": { @@ -1531,35 +1909,43 @@ "status": "untranslated" }, "references/user-options.md": { - "sourceHash": "sha256:ad092f6088edad3d3e8bd124c227f618338b081a453118946880a2fdc8dccf9d", + "sourceHash": "sha256:c1c940b4529b30a1be9a016f78877c647519695975505908b58abf3ce615ed86", + "sourceBlob": "36936949aa34a7fff8e4fb07abe2ad5ccdc6782c", "status": "translated" }, "references/user-settings-files-te2.md": { "sourceHash": "sha256:0a2da6cad41a5e1983662c6d621ab9f78f671aa59f2389f31004b7847deef1f9", + "sourceBlob": "74cf282fbaa6ceb6103de733d816c5dc3adac328", "status": "translated" }, "security/ai-compliance.md": { "sourceHash": "sha256:4c6a93c738e015fe6143730853cffa714e10e80d7911eb6ff5f749dda7a364ad", + "sourceBlob": "5a4956bb3144397fac8b53cbfbd5d66a90e4c6ed", "status": "translated" }, "security/gdpr-delete.md": { "sourceHash": "sha256:25dafde7519c658a7212697ee19bee0033405761a2dfda883549908d63e7ea1f", + "sourceBlob": "202e91437a2e1c53eadf1921e8bdc18bbd716a50", "status": "translated" }, "security/index.md": { "sourceHash": "sha256:77a2eb1494aca34dcd4d36cecd7b1aff207a12ce53e149f69626da621fbecbc1", + "sourceBlob": "a1dbdc12c24f7ab974e64768956f99a48463acac", "status": "translated" }, "security/privacy-policy.md": { "sourceHash": "sha256:3b3d626bc3e999fb30369deca46398b5799282797bf93325e4566fab677fa59b", + "sourceBlob": "35e671a05751e7f6c0a36d57d229a0ada85ca450", "status": "translated" }, "security/security-privacy.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:5d9f2e13cdc658c57cd7a23bcc73bedef02662c7efa62cb1bf1e780e53b281cd", + "sourceBlob": "fb50e726397e3e9f565b8aaecca3838cca6676f1", + "status": "translated" }, "security/terms.md": { "sourceHash": "sha256:90fd51f0cff45e9e16b2c200fc4dc3ccf769b97a3c6afcd09b7a688cfc5b87f0", + "sourceBlob": "ca19992dfe8fa89ca508cbc83c172609d6ab67da", "status": "translated" }, "security/third-party-notices.md": { @@ -1572,62 +1958,77 @@ }, "toc.yml": { "sourceHash": "sha256:11d5df2671e45efed1baf56cd10c6634fc48f5f34b8f40d5b83456258a929108", + "sourceBlob": "b6b79185c71b3a28258ab0af6bd97814d453f14d", "status": "translated" }, "todo/as-cicd.md": { "sourceHash": "sha256:b9c4e2179d5be3177a2119ec147214c8f0e5f19bc82993fce18a1712717ad839", + "sourceBlob": "a6353f134980788549d9de1b2b48c4ccfd06a63f", "status": "translated" }, "todo/Maintaining-Calculations-using-Scripting.md": { "sourceHash": "sha256:f01409f53e2dd517b8504f021d56725efc821bff3e94d5cf25c223f6ee9ed977", + "sourceBlob": "fa43c779e2eff353ce81dd663a8bc2b0cddfdab2", "status": "translated" }, "todo/powerbi-cicd.md": { "sourceHash": "sha256:a564d8b5f8312289b739fa332d0dfae6a3b07822e454da25c6f10c16b1e4c016", + "sourceBlob": "04266dfb17a20c5e49c1a9678ab1b1bf2c41d03d", "status": "translated" }, "todo/te2-advanced.md": { "sourceHash": "sha256:02b6a9bfe2f57d4a8ca9cca47d635343f4dee8b1759807de26faa9ffd51157f2", + "sourceBlob": "7cd22aed96cb2475edbf32cfbdca286c94976d3f", "status": "translated" }, "troubleshooting/azure-openai-connection-errors.md": { "sourceHash": "sha256:e0212bb587e9af4f18ba63af831a01f9e84b893a8306685475e6a55abbef8650", + "sourceBlob": "62dc40b0bda20817c22a73347c263292c141db42", "status": "translated" }, "troubleshooting/calendar-blank-value.md": { "sourceHash": "sha256:0306783580e503f6745682cf15906a60703d87b56ce743eb0f4e04c6b2ed4ca9", + "sourceBlob": "0441b2d4193984196a71087459cbe073fe63a547", "status": "translated" }, "troubleshooting/composite-model-measure-formatting.md": { "sourceHash": "sha256:d9bc88bd4f53f3ede1eb636f1e9815e87abb0b3d6c2d10a68b4af5fb8dbed0fa", + "sourceBlob": "c1beaaa918f415a845e3dfe48c94f4c0ab902183", "status": "translated" }, "troubleshooting/databricks-column-comments-length.md": { "sourceHash": "sha256:5fb9b67014b00e5af4fe732edd4c5b9c99d00adbe3e251f3213ab4d17f2751d5", + "sourceBlob": "8384ed4116a6b07a8705ec2ea293f10a8410f9b3", "status": "translated" }, "troubleshooting/databricks-refresh-empty-catalog.md": { "sourceHash": "sha256:156740404acee951b2e723d390472cc2fed4ec46603eae94c6a8026baf503681", + "sourceBlob": "eee873bfcfd48bf75f4d41fe2959ef9854c9007e", "status": "translated" }, "troubleshooting/direct-lake-entity-updates-reverting.md": { "sourceHash": "sha256:aab510b71ae2d9128c2c1fc565e2e3a3bee5cf2e568fc57aa33a8710b1f650cc", + "sourceBlob": "164e251bce171b4e59ff7a4fe13b4d6d3bf1502d", "status": "translated" }, "troubleshooting/index.md": { "sourceHash": "sha256:201e48fac8aefa81c9dbcf119fc81fc8783ecc822ec5bcf4dd6fc464b8c15393", + "sourceBlob": "a7a225176ee2b15c7abaf6c3e3747e14f8dd92df", "status": "translated" }, "troubleshooting/licensing-activation.md": { "sourceHash": "sha256:1c17fa9f1b9589f06b54c3acd3fa2edc4cb5ebfd428ea71a2dc6e5fb4ab6ac21", + "sourceBlob": "9ad3a2f6b18585fa53faa4b5f52ac3ef986e186a", "status": "translated" }, "troubleshooting/locale-not-supported.md": { "sourceHash": "sha256:3913bccc1fa6aafb9d2d6ab07f4018ea91efa1c1cc34874fc7267ab9b7034091", + "sourceBlob": "aa852b2bcea0af960f5c3c65808971985a321e93", "status": "translated" }, "troubleshooting/proxy-settings.md": { "sourceHash": "sha256:9de7a8f714d448d4dd5184645833cd2a0ca298d2743cae52595ef2ebb83044c8", + "sourceBlob": "a3a5d486c7a589e36b20bada23744006afe59a37", "status": "translated" }, "troubleshooting/toc.md": { @@ -1636,94 +2037,117 @@ }, "tutorials/calendars.md": { "sourceHash": "sha256:42d5fbcac1800e22943c402afc8cc0ab45ecae62d802ffafb492d37a3e4096b2", + "sourceBlob": "dbf139ed2a2573cf48db4ba4254b24295c441f65", "status": "translated" }, "tutorials/connecting-to-azure-databricks.md": { "sourceHash": "sha256:6a47b35772c99b233424bab2318b397f1d866227cc45646e0d11e9c89c4bc9f3", + "sourceBlob": "79fba8437efa8fbd95c8eebf9d72cd774515dbf0", "status": "translated" }, "tutorials/creating-macros.md": { "sourceHash": "sha256:c6c5951e9b76a66c146878235441b06c929595c6d11e86e2a23f8bc00eb5e9d4", + "sourceBlob": "38d8c3225e65db262182e2276a5bfc9acfe66b10", "status": "translated" }, "tutorials/data-security/data-security-about.md": { "sourceHash": "sha256:4a0551c3deb39c692e470f6ea653b5c125c19ab1bad13b81cc117569c5fa20ac", + "sourceBlob": "406353ef6b621c24e83234e5e1b3ee457ccc2139", "status": "translated" }, "tutorials/data-security/data-security-setup-ols.md": { "sourceHash": "sha256:c2f2266fc6f918d57da5fc9347a5c21aee8b769c020eabca32b0fc4408605cf1", + "sourceBlob": "a10baecb1c42450b6792e7dadfd4212e44988bcd", "status": "translated" }, "tutorials/data-security/data-security-setup-rls.md": { "sourceHash": "sha256:af1e2cdd021ffc9ba788cc24eceae8ec1cdca3d4569adaf0b8f271e7e5607536", + "sourceBlob": "2550f485caa3f2ef7292f6a1a4f481bc8f364cfe", "status": "translated" }, "tutorials/data-security/data-security-testing.md": { "sourceHash": "sha256:99fb2630c22ce822b6247189570559ac4bd907a5e19a2b9698ccaf7e7f55a537", + "sourceBlob": "41360870728dbf7d73b6be5c1d2d7eb4f4cd7d8a", "status": "translated" }, "tutorials/detail-rows-expression.md": { "sourceHash": "sha256:db88c79f625f34221cf230c18f11fad62c76fdd79e8a191e54eecfab82156180", + "sourceBlob": "c5b7e3e8450333a7019e1d3826d5b0b5a16e7af2", "status": "translated" }, "tutorials/direct-lake-guidance.md": { "sourceHash": "sha256:964fbdf58e990aee599a3a27f26d5b302cafd8bf85c950a79eebb84f497f1a2d", + "sourceBlob": "a87af70d967a54fb3b1a6c89ce4844609ab99260", "status": "translated" }, "tutorials/importing-tables.md": { "sourceHash": "sha256:1e9205918c6b3e8b7b93dd6c47efe358059e49876878db2233a25a8476257d1c", + "sourceBlob": "ca1a1572338523fded0c48ae9a08d3628911f25d", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-about.md": { "sourceHash": "sha256:4ca05cb2809b0aed7ba334a9f4b6c9ccb053658b4c1cc42dc29562b74cfad42d", + "sourceBlob": "e76ceb0faedf1fcce7bb3d21a621f79ed8f04680", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-modify.md": { "sourceHash": "sha256:9956a6e4fc473c10829c0fcef6ac14a1e569fccd0cfd84d6f3b2cf6fba57c9a4", + "sourceBlob": "087f5c46eb42f5235964b9fb57f9b00742727545", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-schema.md": { "sourceHash": "sha256:73e8125efd00942a78f5931bbe9f0e440742ee8d6de6f54e43a87c992a0ef5ff", + "sourceBlob": "449a21b482c4f9e586a125cee72f121c3e4cc48f", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-setup.md": { "sourceHash": "sha256:9b8100d72880bf60b310d4090727ff8ce66c7da042b85c5558754a66d6508739", + "sourceBlob": "9db8313ca6d20829cb7bc4ab88de6e58aa663c77", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-workspace-mode.md": { "sourceHash": "sha256:8afd82adf5b708dbfd8dc241ca03080817cf808960ba29baf606295334893209", + "sourceBlob": "5596ac89dd389793101181495685a43543666b35", "status": "translated" }, "tutorials/index.md": { "sourceHash": "sha256:08b69c87240fadba66c946ff66b645ca69f32f49a7057d69509af1b6fce4f840", + "sourceBlob": "087430031d41af969abfb553a90fdbc49913e236", "status": "translated" }, "tutorials/new-as-model.md": { "sourceHash": "sha256:617f37b605f71ee0f770b896db1166d532fa0b21fb5e9c084cccfd12324bcbc9", + "sourceBlob": "78979a6f1f9cec1aabf2ba6c4ffeaad88b75935b", "status": "translated" }, "tutorials/new-pbi-model.md": { "sourceHash": "sha256:01b20064a80073530281396fa9208b87035a900d9ed7f608fb741cfc09302af4", + "sourceBlob": "807fddf3f679e02e6c641299b610e140a2ffb60a", "status": "translated" }, "tutorials/powerbi-xmla.md": { "sourceHash": "sha256:bcb5a43d97006c2e63735cda05f2888cecdee8ed344eb20bf4e723d72ab95cdf", + "sourceBlob": "dbcf16ba026efd18ef05a0cba40c8b8b1a2a18e7", "status": "translated" }, "tutorials/toc.md": { "sourceHash": "sha256:8a2515bffeac53dd28f78da28c6c321a4c7687437cb4952039b1f230347996b0", + "sourceBlob": "597c98218ac1b44f1503bdd95cc0c54d5da33595", "status": "translated" }, "tutorials/udfs.md": { "sourceHash": "sha256:4c17768a0dc089431a06fa79ed1785918e2bf24339d93c0976fcbc9781544566", + "sourceBlob": "1d54ec06bb59b37caef36ccb4cc67c82fc24089e", "status": "translated" }, "tutorials/user-defined-aggregations.md": { "sourceHash": "sha256:b5f8ded1057c581a96e8d3f46fea892ea20c77dc028d681125a70abb77290a22", + "sourceBlob": "7b6b8a270ab729cc696e138135f3eba22358001e", "status": "translated" }, "tutorials/workspace-mode.md": { "sourceHash": "sha256:e35e6f82ed64cb2d6a73fe987b64d631ebd2416fba3bb1055e56bb91c89d4eed", + "sourceBlob": "ec8fad8afd55f4c28bc7d290a82e2335fccb7068", "status": "translated" }, "whats-new/3-11-0.html": { @@ -1840,13 +2264,13 @@ } }, "summary": { - "translated": 449, + "translated": 452, "outdated": 0, - "untranslated": 9, + "untranslated": 6, "copied": 1, "pinned": 0, "total": 459, - "completionPercent": 98.0, + "completionPercent": 98.7, "failures": 0 } } diff --git a/localizedContent/es/content/getting-started/personalizing-te3.md b/localizedContent/es/content/getting-started/personalizing-te3.md index d954da178..1739df201 100644 --- a/localizedContent/es/content/getting-started/personalizing-te3.md +++ b/localizedContent/es/content/getting-started/personalizing-te3.md @@ -160,7 +160,7 @@ SUM( 'Sales'[Amount] ) *** -##### [Habilitado](#tab/newline-after-function-on) +##### _Salto de línea después de las funciones_ (deshabilitado) Se aplica solo cuando es necesario dividir una llamada de función en varias líneas. diff --git a/localizedContent/es/content/kb/DR002.md b/localizedContent/es/content/kb/DR002.md index 02b9dce42..22063c57a 100644 --- a/localizedContent/es/content/kb/DR002.md +++ b/localizedContent/es/content/kb/DR002.md @@ -29,7 +29,7 @@ SUM(Sales[Line Amount]) ## ¿Por qué Tabular Editor sugiere esto? -Cuando solo necesitas agregar los valores de una sola columna, las funciones agregadoras ([`SUM`](https://dax.guide), [`MIN`](https://dax.guide), [`MAX`](https://dax.guide), etc.), usan una sintaxis más sencilla y concisa que sus funciones iteradoras equivalentes ([`SUMX`](https://dax.guide), [`MINX`](https://dax.guide), [`MAXX`](https://dax.guide), etc.) usan una sintaxis más sencilla y concisa que sus funciones iteradoras equivalentes ([`SUMX`](https://dax.guide), [`MINX`](https://dax.guide), [`MAXX`](https://dax.guide), etc.) y deberían preferirse para que el código sea más legible. +Cuando solo necesitas agregar los valores de una sola columna, las funciones agregadoras ([`SUM`](https://dax.guide), [`MIN`](https://dax.guide), [`MAX`](https://dax.guide), etc.), usan una sintaxis más sencilla y concisa que sus funciones iteradoras equivalentes ([`SUMX`](https://dax.guide), [`MINX`](https://dax.guide), [`MAXX`](https://dax.guide), etc.) y deberían preferirse para que el código sea más legible. ## Relacionado con: diff --git a/localizedContent/es/content/references/downloads.md b/localizedContent/es/content/references/downloads.md index 8504cc39d..6e6ce6dc7 100644 --- a/localizedContent/es/content/references/downloads.md +++ b/localizedContent/es/content/references/downloads.md @@ -1,8 +1,8 @@ ---- +--- uid: downloads title: Todas las descargas author: Daniel Otykier -updated: 2026-09-22 +updated: 2026-10-11 applies_to: products: - product: Tabular Editor 2 @@ -23,25 +23,25 @@ Esta página ofrece instrucciones de descarga e instalación para Tabular Editor ## Última versión -Tabular Editor 3.27.0.0 (_[Notas de la versión](release-notes/3_27_0_0.md)_) se publica en dos compilaciones: una con .NET 10 y otra con .NET 8. Las dos compilaciones son _funcionalmente idénticas_ y se pueden instalar en paralelo. +Tabular Editor 3.27.1.0 (_[Notas de la versión](release-notes/3_27_1_0.md)_) se publica en dos compilaciones: una con .NET 10 y otra con .NET 8. Las dos compilaciones son _funcionalmente idénticas_ y se pueden instalar en paralelo. ### En .NET 10 (recomendado) Requiere el [Runtime de escritorio de .NET 10](https://dotnet.microsoft.com/download/dotnet/10.0). El instalador `.exe` te ofrece descargarlo e instalarlo; el `.zip` portátil es autónomo y no requiere ningún entorno de ejecución. -- Descarga [Tabular Editor 3.27.0.0 (64 bits)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe) _(recomendado)_ -- Descarga [Tabular Editor 3.27.0.0 (ARM64)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) -- Versiones portátiles: [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) -- Versión MSI: [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.msi), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.msi) +- Descarga [Tabular Editor 3.27.1.0 (64 bits)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe) _(recomendado)_ +- Descarga [Tabular Editor 3.27.1.0 (ARM64)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) +- Versiones portátiles: [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) +- Versión MSI: [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) ### En .NET 8 Para equipos que aún no pueden ejecutar el runtime de escritorio de .NET 10. Requiere el [Runtime de escritorio de .NET 8](https://dotnet.microsoft.com/download/dotnet/8.0). -- Descarga [Tabular Editor 3.27.0.0 (64 bits)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net8.exe) -- Descarga [Tabular Editor 3.27.0.0 (ARM64)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net8.exe) -- Versiones portátiles: [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net8.zip), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net8.zip) -- Versión MSI: [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net8.msi), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net8.msi) +- Descarga [Tabular Editor 3.27.1.0 (64 bits)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe) +- Descarga [Tabular Editor 3.27.1.0 (ARM64)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) +- Versiones portátiles: [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) +- Versión MSI: [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) > [!TIP] > ¿No sabes cuál elegir? Elige el instalador x64 de .NET 10. Elige ARM64 solo en un PC basado en ARM, y elige .NET 8 solo si tu organización no puede instalar el runtime de escritorio de .NET 10. @@ -58,6 +58,13 @@ No es necesario desinstalar versiones anteriores al actualizar a una versión m ## Historial +- 2026-10-15 **Tabular Editor 3.27.1.0** (_[Notas de la versión](release-notes/3_27_1_0.md)_) + - .NET 10 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) + - .NET 10 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) + - .NET 10 instalador (.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) + - .NET 8 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) + - .NET 8 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) + - .NET 8 instalador (.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) - 2026-09-22 **Tabular Editor 3.27.0.0** (_[Notas de la versión](release-notes/3_27_0_0.md)_) - .NET 10 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) - .NET 10 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) diff --git a/localizedContent/es/content/references/release-history.md b/localizedContent/es/content/references/release-history.md index 664bd452b..71d3d52d2 100644 --- a/localizedContent/es/content/references/release-history.md +++ b/localizedContent/es/content/references/release-history.md @@ -1,10 +1,18 @@ --- uid: release-history title: Historial completo de versiones +updated: 2026-10-11 --- # Historial completo de versiones +- 2026-10-15 **Tabular Editor 3.27.1.0** (_[Notas de la versión](release-notes/3_27_1_0.md)_) + - .NET 10 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) + - .NET 10 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) + - .NET 10 instalador (.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) + - .NET 8 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) + - .NET 8 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) + - .NET 8 instalador (.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) - 2026-09-22 **Tabular Editor 3.27.0.0** (_[Notas de la versión](release-notes/3_27_0_0.md)_) - .NET 10 instalador (.exe): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) - .NET 10 portátil (.zip): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) diff --git a/localizedContent/es/content/references/user-options.md b/localizedContent/es/content/references/user-options.md index ff8e2015b..144f23749 100644 --- a/localizedContent/es/content/references/user-options.md +++ b/localizedContent/es/content/references/user-options.md @@ -1,8 +1,8 @@ ---- +--- uid: user-options title: Archivo de opciones de usuario (.tmuo) author: Daniel Otykier -updated: 2026-09-15 +updated: 2026-10-12 applies_to: products: - product: Tabular Editor 2 @@ -25,14 +25,14 @@ Cuando abres un modelo desde el disco, el archivo se crea junto a él y se nombr | Lo que abriste | Archivo de opciones de usuario | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -| `SpaceParts.bim`, o cualquier archivo `.bim` / `.pbit` / `Database.json` | `SpaceParts..tmuo`, junto a él | +| `SpaceParts.bim`, o cualquier archivo `.bim` / `.pbit` / `database.json` | `SpaceParts..tmuo`, junto a él | | Un archivo `model.tmdl` o `database.tmdl` | `model..tmuo`, junto a él | -| Una carpeta que contiene `Database.json` | `database..tmuo`, en esa carpeta | +| Una carpeta que contiene `database.json` | `database..tmuo`, en esa carpeta | | Una carpeta que contiene archivos de modelo en Tabular Model Definition Language (TMDL) | `model..tmuo`, en esa carpeta | TMDL siempre usa el prefijo `model` para mantener la continuidad con las versiones anteriores a `database.tmdl`. -Para un modelo abierto a través de una conexión, en lugar de desde el disco, no hay ningún lugar junto al modelo donde colocar el archivo, así que se guarda por servidor y por base de datos en `%LocalAppData%\\TabularEditor3\\UserOptions\\`. +Para un modelo abierto a través de una conexión, en lugar de desde el disco, no hay ningún lugar junto al modelo donde colocar el archivo, así que se guarda por servidor y por base de datos en `%LocalAppData%\TabularEditor3\UserOptions\`. La opción _Crear archivo de opciones de usuario (.tmuo)_ en @preferencias determina si se crea o no el archivo para un modelo nuevo. @@ -103,14 +103,14 @@ Una anulación cuya Data source ya no exista en el modelo se elimina la próxima | `ServerType` | Cadena | Siempre. Uno de `Sql`, `Oracle`, `Odbc`, `OleDb`, `Snowflake`, `Dataflow`, `PostgreSql`, `MySql`, `MariaDb`, `Db2`, `Databricks`, `OneLake`. | | `Options` | Objeto de cadenas | La colección no está vacía. | | `UserId` | Cadena | Configurado. | -| `Contraseña` | Objeto cifrado | Configurado, _y_ elegiste guardarlo. | -| `Proveedor` | Cadena | `ServerType` es `OleDb`. | +| `Password` | Objeto cifrado | Configurado, _y_ elegiste guardarlo. | +| `Provider` | Cadena | `ServerType` es `OleDb`. | | `PasswordKey`, `UserIdKey` | Cadena | `ServerType` es `OleDb` y el valor correspondiente está configurado. | -| `Servidor` | Cadena | `ServerType` no es `Odbc` y está configurado. | +| `Server` | Cadena | `ServerType` no es `Odbc` y está configurado. | | `ServerKey`, `DatabaseKey` | Cadena | `ServerType` no es `Sql` y el valor está configurado. | -| `Base de datos` | Cadena | Configurado. | -| `Autenticación` | Cadena | `ServerType` es `Sql`, `OleDb` o `Databricks`. Uno de `Sql`, `WindowsIntegrated`, `AadInteractive`, `AadPassword`, `AadIntegrated`, `AadServicePrincipal`, `AccessToken`, `OAuth`. | -| `Cifrar` | Booleano | Se establece en cualquiera de los dos casos. | +| `Database` | Cadena | Configurado. | +| `Authentication` | Cadena | `ServerType` es `Sql`, `OleDb` o `Databricks`. Uno de `Sql`, `WindowsIntegrated`, `AadInteractive`, `AadPassword`, `AadIntegrated`, `AadServicePrincipal`, `AccessToken`, `OAuth`. | +| `Encrypt` | Booleano | Se establece en cualquiera de los dos casos. | | `Dsn` | Cadena | Se establece. Se usa para ODBC. | | `RowLimitClause`, `IdentifierQuoting` | Número | No tienen sus valores predeterminados. | | `Schema` | Cadena | Se establece. | @@ -121,16 +121,17 @@ Una anulación cuya Data source ya no exista en el modelo se elimina la próxima ### Implementación -`Deployment` contiene la última configuración que usó el Asistente de implementación para este modelo. Las seis opciones booleanas siempre se escriben una vez que existe el bloque, tanto si son verdaderas como falsas: +`Deployment` contiene la última configuración que usó el Asistente de implementación para este modelo. Las siete opciones booleanas siempre se escriben una vez que existe el bloque, tanto si son verdaderas como falsas: | Propiedad | Tipo | Se escribe cuando | | ------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `DeployDataSources` | Booleano | Siempre | -| `Desplegar particiones` | Booleano | Siempre | +| `DeployPartitions` | Booleano | Siempre | | `DeployRefreshPolicyPartitions` | Booleano | Siempre | | `DeployModelRoles` | Booleano | Siempre | | `DeployModelRoleMembers` | Booleano | Siempre | | `DeploySharedExpressions` | Booleano | Siempre | +| `DeployLinguisticMetadata` | Booleano | Siempre | | `TargetConnectionString` | Cadena u objeto | Siempre; `null` si no está configurado. Forma de objeto cuando la cadena de conexión incluye una contraseña. | | `TargetDatabase` | Cadena | Siempre; `null` si no está configurado. | | `TargetCredentials` | Objeto | Se han establecido tanto un nombre de usuario como una contraseña. | diff --git a/localizedContent/es/content/security/security-privacy.md b/localizedContent/es/content/security/security-privacy.md index 4365c2bdf..6e97ad6a9 100644 --- a/localizedContent/es/content/security/security-privacy.md +++ b/localizedContent/es/content/security/security-privacy.md @@ -23,7 +23,7 @@ Este documento describe las consideraciones de seguridad y privacidad de Tabular ## Recomendaciones de Microsoft sobre herramientas de terceros como Tabular Editor -Microsoft respalda el uso de herramientas de terceros de la comunidad, tal y como se indica aquí: [Herramientas comunitarias y de terceros para desarrollar modelos empresariales de Power BI y Analysis Services](https://powerbi.microsoft.com/en-us/blog/community-tools-for-enterprise-powerbi-and-analysisservices) +Microsoft respalda el uso de herramientas de terceros de la comunidad, tal y como se indica aquí: [Herramientas comunitarias y de terceros para desarrollar modelos empresariales de Power BI y Analysis Services]( https://powerbi.microsoft.com/en-us/blog/community-tools-for-enterprise-powerbi-and-analysisservices) La documentación de planificación de la implementación de Power BI de Microsoft menciona específicamente Tabular Editor en escenarios avanzados de modelado de datos y desarrollo en entornos empresariales: [Escenarios de uso de Power BI: Administración avanzada del Data model](https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-usage-scenario-advanced-data-model-management#tabular-editor) diff --git a/localizedContent/zh/.translation-status.json b/localizedContent/zh/.translation-status.json index f85892cf9..6612f2d3c 100644 --- a/localizedContent/zh/.translation-status.json +++ b/localizedContent/zh/.translation-status.json @@ -4,374 +4,467 @@ "files": { "404.html": { "sourceHash": "sha256:8e5748cfcc9ca6d991e8e65c36fb2b411cfec8c78e4f7c7be154ca6200c2f80e", + "sourceBlob": "9d9a67896827224867051218fe748baeea2b7f81", "status": "translated" }, "_ui-strings.json": { "sourceHash": "sha256:01a51cba4185663722f9194cdaf9c8c61d9d65dac841e841120c8661b7485fbf", + "sourceBlob": "6bc4e98388d99e464e12eef2a70520fc9e10baeb", "status": "translated" }, "features/advanced-refresh.md": { "sourceHash": "sha256:3d00373433d7f4f8e01331d50c88cd8e11789d9114235a152cccf67ae4800397", + "sourceBlob": "595d6fef56d10834789d1f8265472e19ae61ba47", "status": "translated" }, "features/ai-assistant.md": { "sourceHash": "sha256:f408187864a033df9803ec001397d671d7d2661b82c6aefcdae02e4bf77e22fc", + "sourceBlob": "bddd06ecdb43cbd9bc78b18c27495bc413718fc8", "status": "translated" }, "features/ai-audit-log.md": { "sourceHash": "sha256:b6c0f43580208c5b37cebbeb8cf7937549023f062d3ab2e0700c73b812f365e9", + "sourceBlob": "b99d2865b00c29035f9e1008a651b3ab013ff53f", "status": "translated" }, "features/auto-reload.md": { "sourceHash": "sha256:12a094cafa3b8bcd0d1143ab195cd05c04cce7aceda6d897e9c63276420524fe", + "sourceBlob": "f3813a79cc79cfbf7eb5f4ad66753061c005cec1", "status": "translated" }, "features/Best-Practice-Analyzer.md": { "sourceHash": "sha256:9b948cd95b2378a11d35a1944861ee43f1ea5baa6ccad09e00654e32055e9f5d", + "sourceBlob": "dea840e3b90d43600b5a90815ea1882aec187eae", "status": "translated" }, "features/built-in-bpa-rules.md": { "sourceHash": "sha256:bdffc9c4bc7c941d725928c7b068e04907b5f111d273daacd59d38e57faf0a30", + "sourceBlob": "4028cb601ba4238559d596bf27c4834ae6bac40f", "status": "translated" }, "features/code-actions.md": { "sourceHash": "sha256:82baf7662cf3be05e7a4b399cd9cf3cf6b7ab76eb6eea7233b020be46862b47b", + "sourceBlob": "a7d72c1a74e36ad01c2ba50e5d249dbc24f60d7a", "status": "translated" }, "features/Command-line-Options.md": { "sourceHash": "sha256:510c1728baadeafa301f650793fa1ad76bb6c03b1e2039b2a4d6962ac6bba218", + "sourceBlob": "c2148da283dac1ebe67a27053a7032512a301d5e", "status": "translated" }, "features/connectivity/databricks.md": { "sourceHash": "sha256:9c696bd069c734d7e6b71f06ca44e4d1c5ab2eea9557350ede06b70f6ebc2ce1", + "sourceBlob": "ca07aacf80918aa83cfc89887d270cf03bb810a8", "status": "translated" }, "features/connectivity/fabric-onelake.md": { "sourceHash": "sha256:eeb03bfca0a8c72a8e89d0160bab987cd13e15b5c6d69824de871c68b445bc37", + "sourceBlob": "876294c93ac97b9cce6b29b03f97e5edea129035", "status": "translated" }, "features/connectivity/index.md": { "sourceHash": "sha256:ea13e9c16277ac6b1bc6fc627a8fd8e7fccfef1cd3cedcb0b0a355f5c468e42b", + "sourceBlob": "b7d0f59111937412b5a8751f2db13d576f2df168", "status": "translated" }, "features/connectivity/odbc.md": { "sourceHash": "sha256:3125fb8485c53abb96e4527f599f790f541b94b59dbd5cfff7e01c05a2a7e121", + "sourceBlob": "e49426d53974c10684ef4e52b507e5f6b242f58b", "status": "translated" }, "features/connectivity/oledb.md": { "sourceHash": "sha256:2152c2f02cb41fb7230b3ad5edbe87984fc3573c922cbc20696207eef97acfaa", + "sourceBlob": "596dae2c237609a3423f05f0c93a7a4d435c9ee2", "status": "translated" }, "features/connectivity/oracle.md": { "sourceHash": "sha256:243c9c3d888c4d0a3358e7504a6d75ed00143daf3650e8300b236e92ba5e5bd2", + "sourceBlob": "e6666fba1c9cc9df189efc83e69d83c0f3853a32", "status": "translated" }, "features/connectivity/power-bi-dataflows.md": { "sourceHash": "sha256:0b225765aba34b8efcb135fc393ab717c78bfbfda6510c7e9cefa52dac19c3e9", + "sourceBlob": "0c84343310f1d8541e0e79603c75877e272411f8", "status": "translated" }, "features/connectivity/snowflake.md": { "sourceHash": "sha256:f5abad907ade122a1002b86867ce69d207d051e4031d22693c65d410b79bb788", + "sourceBlob": "d6fd6fd3e1e0a95779907adbf7bd9c9de1c280d8", "status": "translated" }, "features/connectivity/sql-server.md": { "sourceHash": "sha256:44fa78e2f66178989be76fcf94cf80d0a898b789991a3891226dbe30c257fc37", + "sourceBlob": "566a9a774ac1b266a5c15fbb0132b5ecbfc3daa8", "status": "translated" }, "features/creating-macros.md": { "sourceHash": "sha256:b625c28e6d8e1c750f1cd082ec3a475ebcff8e39fd779818024e2bb3c1c10687", + "sourceBlob": "64736b7672327d8e1cf29c922a51b3f8f1baddc7", "status": "translated" }, "features/csharp-scripts.md": { "sourceHash": "sha256:aaf7aebdfc49037aae8aad82cb5d1b8540478a978c9fe3bc46a9acdb962bc63b", + "sourceBlob": "d04304d1be9ea2607b699cfa9aa662ef8020731b", "status": "translated" }, "features/CSharpScripts/Advanced/script-add-databricks-metadata-descriptions.md": { "sourceHash": "sha256:3e3502fa81133433b74714be43bbc8eb911a90f8848ff98d0f1828f56036f9f9", + "sourceBlob": "9f2f9a5953d82619aca182e7e07f1e52a2334c68", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-dlol-to-import.md": { "sourceHash": "sha256:d48972b9f153794a3c9d87f029978e777815eed865588eb6e54d87f2d83b40d5", + "sourceBlob": "2431a3db254213d085ebf92dbe7732fec85da63a", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-dlsql-to-dlol.md": { "sourceHash": "sha256:4680ff0aea6427e90a04380c7c09e6cb98484e75b110d4ac95330e639945d3c1", + "sourceBlob": "a2d82652bb9a738a0f3cf9294526a43b5ff91141", "status": "translated" }, "features/CSharpScripts/Advanced/script-convert-import-to-dlol.md": { "sourceHash": "sha256:db70a79ac1d2956f2b18a83b83c3693704eb1235b271697b020432e3d0a07990", + "sourceBlob": "98f5f70405bbec01c36fbafe83d0087d9e023a63", "status": "translated" }, "features/CSharpScripts/Advanced/script-count-things.md": { "sourceHash": "sha256:67e92ecd96ccbe2ec1373a8d3280182a126d832821001b70e21c990a4742deb2", + "sourceBlob": "da055364a63fc18aeec17804379b0a16eeacab65", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-and-replace-M-parameter.md": { "sourceHash": "sha256:0ce06f6f7719671d46c05fabbbc25a25721c9069615c6484b6dbe2bf058b8505", + "sourceBlob": "fa748cacfa71afe6e19286cfbb041e94cc409a48", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-databricks-relationships.md": { "sourceHash": "sha256:7100c6e93a2961e64c4962a5e22d0bca278b2f5c97b1c4509fbc20be236b7b5d", + "sourceBlob": "73730477e488ef890341f776cb7f7576ee312d3b", "status": "translated" }, "features/CSharpScripts/Advanced/script-create-date-table.md": { "sourceHash": "sha256:a95991c83be2e3b7d7fbfa7c8b1eff5f59467487b66aaed2fe7a43e510f002ca", + "sourceBlob": "d214bca353a75f93a4ead8ffb10419acae0ab08e", "status": "translated" }, "features/CSharpScripts/Advanced/script-databricks-semantic-model-set-up.md": { "sourceHash": "sha256:d18ee6c4ecb762d70373d3632b828a41a5dd76b283a0f742065aed5bc9ce0233", + "sourceBlob": "4c3e4fc27c7263a2a08f8ea9dced2fd7f842be2c", "status": "translated" }, "features/CSharpScripts/Advanced/script-find-replace-selected-measures.md": { "sourceHash": "sha256:01f8139d9460fda1438695c2ff55c02a0671fc22ec442b111dd44236cd0f22c1", + "sourceBlob": "f80005ea3fb552e9a84cea242d690fa0389a7727", "status": "translated" }, "features/CSharpScripts/Advanced/script-format-power-query.md": { "sourceHash": "sha256:a18bc6f534e163a0cc87d75f47b02f13c3f6d47d4dd988ce53f55bd52002126b", + "sourceBlob": "c936182183ffab844cb70911d57f98ae6ce35153", "status": "translated" }, "features/CSharpScripts/Advanced/script-implement-incremental-refresh.md": { "sourceHash": "sha256:ebeaff67ce308880b8ba5be4b9cd198c30a5f48af87df7b477d48ed313bb1907", + "sourceBlob": "c61080670ead87cc586cdd71acaeffef655af7c1", "status": "translated" }, "features/CSharpScripts/Advanced/script-implement-user-defined-aggregations.md": { "sourceHash": "sha256:e4a1668f2fe37b9dfedf705f1ba2052a3f696c10e3422420ee24e7f9a983aa8c", + "sourceBlob": "c0fad152d810aff8d047fc97a1cdfc33dd2b75af", "status": "translated" }, "features/CSharpScripts/Advanced/script-output-things.md": { "sourceHash": "sha256:b610ce223647b635018c8e048c00b9004cc93ebc2309db26c30b383e9497b0fe", + "sourceBlob": "2a7b4d9fd9bf7aee8e1e3de2fbe07fc501bfacd8", "status": "translated" }, "features/CSharpScripts/Advanced/script-remove-measures-with-error.md": { "sourceHash": "sha256:d0a4c46a77e8af7bb00b109e8875d4b549bc0c03f22ec28d2f1bd2d95ce4f20d", + "sourceBlob": "0291bce2f09b6dcd79787ed8cbe3013fe3514f9f", "status": "translated" }, "features/CSharpScripts/Beginner/script-count-rows.md": { "sourceHash": "sha256:691212c34c7c4dc0b39809eedd600fa594cdcb8d8e0d116fa9e6804a07fd1088", + "sourceBlob": "843cec9bedafb0b804bd02bda264250c2b65bc69", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-field-parameter.md": { "sourceHash": "sha256:68a3acca96d3e372683c73129f700024e3229daa1752be3805653accc56fd91d", + "sourceBlob": "16b5b229f315eefaefabc80610a86ae119290c5b", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-m-parameter.md": { "sourceHash": "sha256:9e2f4adee2211c2ef4a90fb017b289dbe5bdd935e290172030e5b6f32f1bf2bb", + "sourceBlob": "7ddee7e50a4a7287e21f7a1e9e0c2ea0f6c8412a", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-measure-table.md": { "sourceHash": "sha256:0a641a1fa1d5b7b7ab73799b15d9434407b3fecc1d721ed1d43a58a4ff1d30e2", + "sourceBlob": "85dcab0922799ef70ded284fa5d15ca36994457f", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-sum-measures-from-columns.md": { "sourceHash": "sha256:50bd3f2a8ef34e438493f91da92be743f7cd4296a694d3243460abeb47f28c89", + "sourceBlob": "752a10e104f215457dc2d3c15eb2e55323aa4041", "status": "translated" }, "features/CSharpScripts/Beginner/script-create-table-groups.md": { "sourceHash": "sha256:74dd9a2bda903d85972caedcf9e230ca4843fa87fa490bd82122498db8a261c4", + "sourceBlob": "7f89ea59485e72f3b39dbcfb854581bccf88e1e7", "status": "translated" }, "features/CSharpScripts/Beginner/script-display-unique-column-values.md": { "sourceHash": "sha256:9305105469bd45fb0ce7edbb66fdc9c7ec9be16d377bca79c8265ca16451aeeb", + "sourceBlob": "1dbaf8539bacd99ba9608b83094f0dbf16aca869", "status": "translated" }, "features/CSharpScripts/Beginner/script-edit-hidden-partitions.md": { "sourceHash": "sha256:fe1dfb281e64a7367180a7705a5559be43d8c9368e1c5e7acccab093ee1430d4", + "sourceBlob": "967b133b0bd871b6d17ed115a6c723f7c6075e45", "status": "translated" }, "features/CSharpScripts/Beginner/script-format-numeric-measures.md": { "sourceHash": "sha256:e2c47703f04f15d22d23fd66c8abda9a27e5ff15ffbf6c9e73a9dbc54ba3952d", + "sourceBlob": "8057806143c71360baf5228c35dad7b95208ade7", "status": "translated" }, "features/CSharpScripts/Beginner/script-show-data-source-dependencies.md": { "sourceHash": "sha256:6bd1324dda48a5363b2d1e9d9e893f61f2e3364913c64848180c5069442a1600", + "sourceBlob": "c95f1107c52231e316455c302c29650093e5c00b", "status": "translated" }, "features/CSharpScripts/csharp-script-library-advanced.md": { "sourceHash": "sha256:1bffc5de50a63a02a1499b6c690f8f021a456f69bcea94de8d30589792f76e9f", + "sourceBlob": "8dac2ac36c442c6d19cac96b775a2d447aa7531c", "status": "translated" }, "features/CSharpScripts/csharp-script-library-beginner.md": { "sourceHash": "sha256:8297f36b1b4788ff484bc7fbda33135465e258039c2c4fb91a10d7dd8d8dceb7", + "sourceBlob": "544e3bd8ad1015ddd1fdf1e20f4194d3b666b4b5", "status": "translated" }, "features/CSharpScripts/csharp-script-library.md": { "sourceHash": "sha256:5097363cc7a2ae52c4ad14596dfe2e963ba391ff21942bd0e472c45dca814ec7", + "sourceBlob": "052c9c79b00fac845af6faf3dfc417537b257859", "status": "translated" }, "features/CSharpScripts/Template/csharp-script-Template.md": { "sourceHash": "sha256:d33ce23f236296d5f6a22ccda044b9cc2cc97b36fac41cfb8413f76c85c56af6", + "sourceBlob": "11ad23eb33ddda31b959741c47ba60d8998bdfe8", "status": "translated" }, "features/Custom-Actions-hidden.md": { "sourceHash": "sha256:cce28f9efc24691ee1e3b025b1d5e87244c1dd656d8d4fe1226518fdd05fb40f", + "sourceBlob": "82152e8036fb5839a393b6dd935f69f079a3f16d", "status": "translated" }, "features/dax-debugger.md": { "sourceHash": "sha256:2edb8f85458de285384d61b1bd055fda7ff1f02e451a601cb555acac25c64f50", + "sourceBlob": "daa9f9eb4216be244ddabc99d2c24b79ee6adfac", "status": "translated" }, "features/dax-editor.md": { "sourceHash": "sha256:b8456895d28fb1c0ac0e2e78432a85e2d85bdf9ae2aa1e2d9c9edb2784e55923", + "sourceBlob": "b6dbdb572a7d9e9121ec6dc8e6d51f10018dadb5", "status": "translated" }, "features/dax-optimizer-integration.md": { "sourceHash": "sha256:7f360b8d39e0a763558717f52a112f4e908cd7e30b99c94580b0f478277858f1", + "sourceBlob": "187aca039f9ad1bf5d08fd48c59bc1c1cdbe973e", "status": "translated" }, "features/dax-package-manager.md": { "sourceHash": "sha256:deb8cbd2b8ad4c16f627cfd8b12f903da4b08287cf955f14cc1a8ecddfc7a825", + "sourceBlob": "f2b54efe53008a5ff258010a9b9c7dd8ba9ab035", "status": "translated" }, "features/dax-query.md": { "sourceHash": "sha256:43d7d61f433e79310e7af7d7e44e34ffa6f80b49df061b34d91f9285a55131d4", + "sourceBlob": "8c8f8290b7fc114f9a41133437cb64cb7541c74f", "status": "translated" }, "features/dax-scripts.md": { "sourceHash": "sha256:68a0f9afca10252a6d2c9e4c92642be0d725368a46af4c4a59b7420756d92d83", + "sourceBlob": "48ee7a19819e514fd13026e9a5abccd350531cb6", "status": "translated" }, "features/deployment.md": { "sourceHash": "sha256:ccf07d6fb575c9a62fa94e4e5b3ba9e41543a57b1b651441ecdde3eabe98b0f2", + "sourceBlob": "e8b2cef8f04a3e015825aab2432448e6763e29cf", "status": "translated" }, "features/hierarchical-display.md": { "sourceHash": "sha256:45963c4ab2a25c88ab2bcb5a8115e1569ce71098b36c09b4f3c7e8663dfb1f32", + "sourceBlob": "bcc131845c0deb904311df84339e39f4a6c657e2", "status": "translated" }, "features/import-tables.partial.md": { "sourceHash": "sha256:b416ea2b5d09ea7cc44d41ae621ca79a122104ad088f8c8bcd3c7d320ed88431", + "sourceBlob": "586e2c6455a8d676a85c09d69628d9d3f7006e9b", "status": "translated" }, "features/index.md": { "sourceHash": "sha256:523bfe7e887f482e3eac8691e77879feeac70030cd508acc736994622c6db683", + "sourceBlob": "9d66dff2ab0078ddefdc944fda46cd40ebb22951", "status": "translated" }, "features/mcp-server.md": { "sourceHash": "sha256:9fbfc101f7c6f4a71721ef6382610bdec5123aa3ddd0f4728ee93852bf4f8196", + "sourceBlob": "1915106ad79557a9fc86e7c87bc600f974e04249", "status": "translated" }, "features/metadata-translation-editor.md": { "sourceHash": "sha256:7dd826b950714972b3429347f09fed22bcdbaf3f149e4f47dc6382ecf26e69e5", + "sourceBlob": "0aadd2b697b21873edd16fbee8e453c8883b7138", "status": "translated" }, "features/perspective-editor.md": { "sourceHash": "sha256:d7bd4b9834bb8fac3a1072e4c84d43efe36d64e7443b50f0778f0f76f189476f", + "sourceBlob": "b823606a62a57145477463f74a99fec5abf35bf7", "status": "translated" }, "features/pivot-grid.md": { "sourceHash": "sha256:13469e84b4715d3989eb9f9220222807cad8eaa30bb5dc6eb1837da1e13d82ee", + "sourceBlob": "2ec2cc595eb7bce1320d14aee51d119bc658d104", "status": "translated" }, "features/refresh-overrides.md": { "sourceHash": "sha256:8ebe84c4db022e87ff73603eed7974660e6edab0fcc5a8f416b1290a6f05e413", + "sourceBlob": "953a055ad5f45c6d57947adba38711895375a8c6", "status": "translated" }, "features/save-to-folder.md": { "sourceHash": "sha256:a0787dde9336e2f631234d0d95ec8519ccdb84a1495325090c56e9ec8caeee08", + "sourceBlob": "32096acf2a61616cad34cfb6dc26be9b8c9828d4", "status": "translated" }, "features/save-with-supporting-files.md": { "sourceHash": "sha256:a1b88d752e28fed5063904efd4c743e043cb3fe28591b6cde5a8182d00130b41", + "sourceBlob": "42c8e358e5865899c7318909c20cfe907c2c7879", "status": "translated" }, "features/script-helper-methods.md": { "sourceHash": "sha256:5cb24ef463fa580075848ea2facacb0b906a1e08ff6cb1c62f7e41ae3824fbf6", + "sourceBlob": "0e6d57a40fa73f9c43cf764fc9d9534a2e913e74", "status": "translated" }, "features/semantic-bridge-metric-view-fields-and-dimensions.md": { "sourceHash": "sha256:58bd40eea2c9da6f1cf4b1b24ca50f04ed021a171289ba554edccd4874fbafa4", + "sourceBlob": "04720649dd3f611b9bd31a3e139dac9bea1321f4", "status": "translated" }, "features/semantic-bridge-metric-view-object-model.md": { "sourceHash": "sha256:6028084095ef69f4cd3c5f0e0770809dafca75ba3636e9f02335c7984e06c826", + "sourceBlob": "c6a1faafd947b0d7d74f86730285ee36cb63bac7", "status": "translated" }, "features/semantic-bridge-metric-view-tabular-translation.md": { "sourceHash": "sha256:74be78dd2533ebab27bb4d9dff59a263f12f19b90e8bc746d6206feb4d7f08b5", + "sourceBlob": "92ffa33e53833d234015db9a287e0e8a1d621ad2", "status": "translated" }, "features/semantic-bridge-metric-view-validation.md": { "sourceHash": "sha256:e2f6dddbee46537b3923de827539556524d25ad09b5125e2df1510eac271179c", + "sourceBlob": "0dbe93b10b900b5b49137651c119bd2877d30a7f", "status": "translated" }, "features/semantic-bridge.md": { "sourceHash": "sha256:148612d364d9d289779fdf6bb5119e69a5a06f1162e11a909c5456284fbfcdbb", + "sourceBlob": "14794316b4726ee655ed893d0fb88318451c4184", "status": "translated" }, "features/Semantic-Model/direct-lake-sql-model.md": { "sourceHash": "sha256:7f45a897ce4afe418c209f05ba10729e24bba2a60d8710191bfd66051336293b", + "sourceBlob": "ea02d4a245095ae863fc83d1f8efaea45008b918", "status": "translated" }, "features/Semantic-Model/direct-query-over-as.md": { "sourceHash": "sha256:280fbda0a57da7ba6b11294d3809f7b52c4f25cdf7ca5fa07e85264e5cd1e74d", + "sourceBlob": "9f0d50abd8e16a26b6ddce3b950b418e250b9df0", "status": "translated" }, "features/Semantic-Model/semantic-model-types.md": { "sourceHash": "sha256:c6bc2434213fc85c0062d07e80f8e0341d4375cce84561d770944e00a7f1b1d0", + "sourceBlob": "a7d91a750887b553c8810790fd860f03eefd5480", "status": "translated" }, "features/table-groups.md": { "sourceHash": "sha256:5459da04f54b462c03fc23a93c0ea70583db05f352b5d1fa4eff8de6fd789586", + "sourceBlob": "b15486c461048b998fde6c32bce176e758bdf2f3", "status": "translated" }, "features/table-preview.md": { "sourceHash": "sha256:919ddbf231d1336651625f666704024ac80ce09b637d0c73ccb94aba492afc9e", + "sourceBlob": "9d7d9bf467d7d9b87945f6318da3f2c158f3d1af", "status": "translated" }, "features/te-cli/includes/te-cli-preview-notice.md": { "sourceHash": "sha256:78d64bd2aeab6acb614ba5793a522de28cc7726655789e476ee50e267f561a46", + "sourceBlob": "e9dbcc25c59eec001190d9f25f394af5650861ce", "status": "translated" }, "features/te-cli/te-cli-auth.md": { "sourceHash": "sha256:7c546bae4a34fb5d356b48977274fdb0b106bc862f89d09b59fa857ac78a962e", + "sourceBlob": "3348d29f26b1a64a4154901896bb2f0195124d21", "status": "translated" }, "features/te-cli/te-cli-automation.md": { "sourceHash": "sha256:4816d4daf4c65c11daeb777419b0d539d79055120d6516ab432b7567961fc8f9", + "sourceBlob": "f67dfbba4a2cedad06b78a5f3cf17da282f5a858", "status": "translated" }, "features/te-cli/te-cli-cicd.md": { "sourceHash": "sha256:3407d54cf37c954a2e53d190c14f84502a04f602c3d1605d1e040f479eb57381", + "sourceBlob": "af1939b97325f2b52f820a21f08a53d50af5994e", "status": "translated" }, "features/te-cli/te-cli-commands.md": { "sourceHash": "sha256:3002131eec0fe7c2f8e318c6bdb7227792ff24970f88738da20a4e900af573eb", + "sourceBlob": "3e0f0ae6fa8f17b069755ac733e72e9a460b5076", "status": "translated" }, "features/te-cli/te-cli-config.md": { "sourceHash": "sha256:b9778fc392a9c0f2ec9631c4bda5a71fbb8161103102b2e10f923a0f68205a64", + "sourceBlob": "cb3f4b80a49df21e8f49aee372a54bb3a2a56ae5", "status": "translated" }, "features/te-cli/te-cli-findings.md": { "sourceHash": "sha256:7fbebd98ae9b0954f975e87b2d58ec49c888349ac4e51e5399fd87642dab8ce9", + "sourceBlob": "ebd0602375cc90e65a87a64313230be2c2336871", "status": "translated" }, "features/te-cli/te-cli-install.md": { "sourceHash": "sha256:3846cb039ee6af3f7839e1dc9c3eb4581ae64d35ebba3155ce3e5c0e1e112fe8", + "sourceBlob": "7e2c09d12d14142c566a1bc0f2f7f165f5e75a3e", "status": "translated" }, "features/te-cli/te-cli-interactive.md": { "sourceHash": "sha256:0c276940dcfc72aaed58827820de0b1e253113c0b10a97401cc5c44eb1928663", + "sourceBlob": "434b92b9def4a864b3aac654c1bf17a81faea23c", "status": "translated" }, "features/te-cli/te-cli-limitations.md": { "sourceHash": "sha256:03f4d5fd792465b3ba3aa6e3f731937ca34648c5a8820936ad2b432da7dd66de", + "sourceBlob": "11470df38041c9efb886d4f233b7016f233aa243", "status": "translated" }, "features/te-cli/te-cli-migrate.md": { "sourceHash": "sha256:1bb8b09850ad6534ba7da224d4fd26365b24f27ea1e181031619fdb926130478", + "sourceBlob": "92f4fc0f6b5a7ff13ecee6a6090d10c1f00cc721", "status": "translated" }, "features/te-cli/te-cli-skill.md": { "sourceHash": "sha256:55eb8144d60ffde111240da39e89c8ea98390d868e79363c70024ddad541d8a9", + "sourceBlob": "fe2cbae27138b4ffa18c36a2b5df4e85c2c75b9e", "status": "translated" }, "features/te-cli/te-cli.md": { "sourceHash": "sha256:3809890dbd0821611a2300d79384b21997019d0cccf0e713d81ff2862da558b0", + "sourceBlob": "8c5719ca18d018255956508c615509bc0d198e68", "status": "translated" }, "features/tmdl.md": { "sourceHash": "sha256:0200e992b3cc0f6ea5a18960a70bc722d4f6343d20001ec3ef23f724edbd180e", + "sourceBlob": "3dfd0667b205942cb0535306493253abc97adcab", "status": "translated" }, "features/toc.md": { @@ -380,170 +473,212 @@ }, "features/unsaved-changes.md": { "sourceHash": "sha256:b4276f4a2d3144459dfc5e6fa6f6e71d560547cad60cc6c6227c8a2207fc3651", + "sourceBlob": "01f50b550f63849d244f6006fe95f8bd8c19ef36", "status": "translated" }, "features/Useful-script-snippets.md": { "sourceHash": "sha256:c8a0ddfa6f8ceadc38a222a3c5155f2da992c93f1fe4297a629251f1b5b95c28", + "sourceBlob": "c6fc2d702972555e1b16467df49ee861f16b4e74", "status": "translated" }, "features/user-context-calculated-columns.md": { "sourceHash": "sha256:2d7a43f84729d3d925e4d918752c57a05335335e8af7547bc7f829a833e46751", + "sourceBlob": "50940f82680787205217026bca712a95e81ae285", "status": "translated" }, "features/using-bpa-sample-rules-expressions.md": { "sourceHash": "sha256:5f01197ad9e53d65cb6c7756372e4827a31e994a0a70a16a8aaded9b52339515", + "sourceBlob": "c1e81f45fef0c94967f1c517dc46dbbe3c3d8b97", "status": "translated" }, "features/using-bpa.md": { "sourceHash": "sha256:ea063d06bedae22da258863a7f4727edb1be9b9751602681d1a9a23d66950ee7", + "sourceBlob": "86c47854b91121eca8f2b754e32629daaad2640e", "status": "translated" }, "features/views/bpa-view.md": { "sourceHash": "sha256:dcccdbd044bbd074e52b5abf8e72d8ec7b714d81617f0261b49ab8a776de1670", + "sourceBlob": "f13334c63f552805fc2b3d50a215047a5fc4c82a", "status": "translated" }, "features/views/data-refresh-view.md": { "sourceHash": "sha256:6427f5aec9357680f68f96702ca980623b7f518f505ca0a179451aa63d701b15", + "sourceBlob": "8c9ef9694a63cec18b6fac328ee4a313c2f308f4", "status": "translated" }, "features/views/diagram-basics.partial.md": { "sourceHash": "sha256:6159325df46084d452d7adb3e28b85ba4da85bd270c9e158624ea2a16653d182", + "sourceBlob": "efdef982ec12e4c57b53f29fc58582d9f5ac36ab", "status": "translated" }, "features/views/diagram-view.md": { "sourceHash": "sha256:55ff4574e2a9a8bfc1f92025e34a95a42046daef4cf96315b7c64be03b935796", + "sourceBlob": "d9b5d85f347f739c28091049bb3b69fd3ddc4314", "status": "translated" }, "features/views/find-replace.md": { "sourceHash": "sha256:32a2e329d26e3712c1115ee0cb1965780387888d05c5af11b7bb2b056c8f3d42", + "sourceBlob": "e8ea546897ffdd36d31f043833f4cb1a374636ba", "status": "translated" }, "features/views/macros-view.md": { "sourceHash": "sha256:6f8046bc8d5bcf581c147a3263fefeb4f6dbc160cccd29a0a8b09db2949f055e", + "sourceBlob": "371f9dd56bdc565549d82dc6e330c8bed888b8b2", "status": "translated" }, "features/views/messages-view.md": { "sourceHash": "sha256:c444a771025eab4067d298298fa8391fd1e0157c38582320b097f9d2320d4657", + "sourceBlob": "a48bd92d4addbe14a03f74ce7afbadedbabea93f", "status": "translated" }, "features/views/properties-view.md": { "sourceHash": "sha256:3de52f392fb38a6066663c222690aa9b42ad3d611496e76463eed2c2c416faf6", + "sourceBlob": "0de8aa4db131f025fb45835d377d073d3df0bb30", "status": "translated" }, "features/views/tom-explorer-view.md": { "sourceHash": "sha256:620e8d14dba16e4b5bdbe6ad7beff5b79903b29551ca338efa94d6efaeb041a0", + "sourceBlob": "91969425bae2aed64fa70aaa2fe852341d14de15", "status": "translated" }, "features/views/user-interface.md": { "sourceHash": "sha256:7e6a431584ea6e620b932f5ff895cea2a9a28faecc253b212fe62b033ed833c7", + "sourceBlob": "10005d97d86dca7ddc750111b0862c0936c2cb43", "status": "translated" }, "features/Workspace-Database.md": { "sourceHash": "sha256:3e8d2cab08786abac392997434c23ebe54b1bd2b7e1158513819bf366060c0ee", + "sourceBlob": "50cffd07b076258eed8fd54b4fa71126fde31c57", "status": "translated" }, "features/workspace-mode.partial.md": { "sourceHash": "sha256:e05d93903abab9005d7a7d2b03dcb4f65b0e651c4456beef68750dfb44fabc2f", + "sourceBlob": "d7067641fdb0151f99050a21c69b16732db7355d", "status": "translated" }, "getting-started/app/index.html": { "sourceHash": "sha256:1d58f022bcf30ea4582377c3145def5edc9c8dd96c25abbb66a9ce4b6918c457", + "sourceBlob": "12c587ca046b180a307a4e66256db0319db5ac5c", "status": "translated" }, "getting-started/azure-marketplace.md": { "sourceHash": "sha256:aa36a1b4f471f78d88e57402405aec43b37ccd2d1a53a1444ab1731de18c88b1", + "sourceBlob": "f2d05b99fec5603416ae3a279026081a878d53d4", "status": "translated" }, "getting-started/boosting-productivity-te3.md": { "sourceHash": "sha256:6cbd04a7b267e22d41c812a2efedda6cfc840a8c9e7146b0144cae6a1b077a61", + "sourceBlob": "ff0edf619e6010c0a164b69d7781cec92c169845", "status": "translated" }, "getting-started/bpa.md": { "sourceHash": "sha256:dd602ec731cd29ebb700cf7377396fee262113d74c9c584d8dd17dd46c21fba1", + "sourceBlob": "14c8010c6f7ed5500f707a0b9e10a4d5c674cd03", "status": "translated" }, "getting-started/creating-and-testing-dax.md": { "sourceHash": "sha256:326a4af18f1bacea3e4b6a52d44c336bab3495074918c9cf49722d2e579a561f", + "sourceBlob": "dab9ba65e0500cb21f67b7a6218c83d5663e7197", "status": "translated" }, "getting-started/cs-scripts-and-macros.md": { "sourceHash": "sha256:4c587caf1e8d5a60f4ce6d1e6c9181ba8469b6b46c4dd315239689590ec4b135", + "sourceBlob": "f78d1ab47a62093e17dc33851af6f048962eb688", "status": "translated" }, "getting-started/dax-script-introduction.md": { "sourceHash": "sha256:1c500ed5798529eb123c3f097b3052dd48a97e2bb4a6fb8e2ff8a3c0660048ae", + "sourceBlob": "9afd91645a2433cf7b2e627e5a24e2705fd7e5ea", "status": "translated" }, "getting-started/desktop-limitations.md": { "sourceHash": "sha256:642629068eda1994deaeed8eb17e811e878c3f9fa4d2b73d20d0ca65e87568d5", + "sourceBlob": "80696f124337c358bdea791ac18aedb388a1eccd", "status": "translated" }, "getting-started/editions.md": { "sourceHash": "sha256:f7d969bcc3e0464cabd627045f03454978e6bb68ff4ffc754694dd6c89e07cd4", + "sourceBlob": "25c0ca619fda06ba57679a8ceaac6fc35d5742b3", "status": "translated" }, "getting-started/general-introduction.md": { "sourceHash": "sha256:310091474ffb682608d139342191400badf995a700cbc54e539b70e1fd879f79", + "sourceBlob": "cb8d9383005d76762c8ffa4bfd4f14d9f463c299", "status": "translated" }, "getting-started/Getting-Started-te2.md": { "sourceHash": "sha256:f7f3fc16ac57b499364b970568de8ccd8b6fd1d2122ddcfb4bca709ac5af9776", + "sourceBlob": "645a3670337ebca30537d0a4013f0ad0d08b9bd2", "status": "translated" }, "getting-started/getting-started.md": { "sourceHash": "sha256:a0265501f2a95f673ce169db86208950eec6e2523d1d4f3e0d241b3372d53e96", + "sourceBlob": "d703d438dee570508d4e6bc3e223975be91b9530", "status": "translated" }, "getting-started/github-flow.md": { "sourceHash": "sha256:8a6cff7e623f0d857474b867d285f3a408f81023160652a167ed79531037343a", + "sourceBlob": "a80125cade3d4f2402ffd4a7629c267922f96492", "status": "translated" }, "getting-started/importing-tables-data-modeling.md": { "sourceHash": "sha256:43389d4ff2a11ad55cbcebbcc1550cfdd551c32fbe31dc2f5606340e4c3e57d8", + "sourceBlob": "64f9ac7f1bb6d128a2740ddede62941604171ece", "status": "translated" }, "getting-started/index.md": { "sourceHash": "sha256:04757c50e64426cab3b67466495644cfbd6bd3bfed8379473d675e5bc2ca4ebd", + "sourceBlob": "5c4f8089e16f0b76bfd1a176bd640da97a30c6e4", "status": "translated" }, "getting-started/installation.md": { "sourceHash": "sha256:ef3f254c8f8def44e84251f71974aec719f3b23b69dd37d3c5bc343b3a53ed8a", + "sourceBlob": "2435ed67e1a53b4e2e21f968e9e469187862fa6c", "status": "translated" }, "getting-started/migrate-from-desktop.md": { "sourceHash": "sha256:073cd52677a718dba308dfac64ba1f27b76ba4534f77ddb99b2b4447c1511f70", + "sourceBlob": "b1fc8b39d6efcdfb61f7ac50beece138b8666a16", "status": "translated" }, "getting-started/migrate-from-te2.md": { "sourceHash": "sha256:e3b8f23bb9f7f7e7e0a911db6d0de1ad7f6479cdeed5e6093421e27ff6ce3c20", + "sourceBlob": "487a4e16fdd4dd8b6ac57331a6ef68c5155cac28", "status": "translated" }, "getting-started/migrate-from-vs.md": { "sourceHash": "sha256:e828a626484397cb46e2a36b35675c639dbdd5e17976c21967f337deafbb9f4f", + "sourceBlob": "219da009cff71c8ac336668cedead910296cd0cb", "status": "translated" }, "getting-started/optimizing-workflow-workspace-mode.md": { "sourceHash": "sha256:be881580d2cef038fc74e503591f8110998d40922a7c30bc15936ad7a36dc823", + "sourceBlob": "3a930229e6e6fc71e2ffcaf87336a8b56a9efd07", "status": "translated" }, "getting-started/parallel-development.md": { "sourceHash": "sha256:25d9494276172d9ffcdb40b37e6d5cceee10afd48c413a7299e5f686be0fb613", + "sourceBlob": "71cb81c4c14c4e69c7e259f8312f4fb2290e4c95", "status": "translated" }, "getting-started/personalizing-te3.md": { "sourceHash": "sha256:0e2b6a13cb3a279d40f713d56287aca1a7d6d340ebec4e13842b3d51b42f76d8", + "sourceBlob": "8f3836fd27c04e5868769a605ebafa53aa37b130", "status": "translated" }, "getting-started/Power-BI-Desktop-Integration.md": { "sourceHash": "sha256:f725405cfbeeb34f15100aa477e813197a3d40640fdcd2de82f2262cf3b41ef8", + "sourceBlob": "94d771d91a0b9665b82941bbe23e530e73494b03", "status": "translated" }, "getting-started/refresh-preview-query.md": { "sourceHash": "sha256:3bdec39f290189d4f7a9e60eebcaf4eed6a54db750203a2d1bf692927d4fa5c7", + "sourceBlob": "013c21f02eca15be302eca2ce4059b0850003091", "status": "translated" }, "getting-started/sharing-macros-bpa-rules.md": { "sourceHash": "sha256:a2c174930bce002fa8691b6585917c06845079b0f8cd1b03133707c45fddf91d", + "sourceBlob": "9b691989ed5f710213f587085944acb6c66ef976", "status": "translated" }, "getting-started/toc.md": { @@ -552,242 +687,302 @@ }, "getting-started/training-telearn.md": { "sourceHash": "sha256:cba2b907335edf7e1310d6323c063305cbed3c66013758b932d91466394b4d85", + "sourceBlob": "b397eab6d24a427124a93029d1ca1291c697c75a", "status": "translated" }, "getting-started/views/bpa-view-reference.md": { "sourceHash": "sha256:15ca9b6b41ad2b4a120b6ef5ea17c4474e72fa68375e5539654e662057eb2383", + "sourceBlob": "9c7382004050c2209a0562f6849ff164472fb554", "status": "translated" }, "getting-started/views/data-refresh-view-reference.md": { "sourceHash": "sha256:5ae489e72f396630aec7b81960492180ce2359df98a403503cc7613fd2f22049", + "sourceBlob": "c85ccc03ce66f1e851186a77121fcbb8a7e99900", "status": "translated" }, "getting-started/views/diagram-view-reference.md": { "sourceHash": "sha256:dfc344df0e16e8b8e186954e9cee6883ce21b968351b5d590947c24c9219617e", + "sourceBlob": "72755e9142cd6ea7a41571b151b44af7a66b4e85", "status": "translated" }, "getting-started/views/find-replace-reference.md": { "sourceHash": "sha256:a207ceafc5c8e3d3a218c80d405fafc3c71a9d9491964366607ee205e28a2b69", + "sourceBlob": "ba9c3fe3d81b4b88801c5ee36d1f549b785f2c60", "status": "translated" }, "getting-started/views/macros-view-reference.md": { "sourceHash": "sha256:684b3ae341bc7955f0f304e04782f7b80505ca4bb2490a7c91e99e19439b743a", + "sourceBlob": "9011cd129c99784016784243f0b4ee02a2563935", "status": "translated" }, "getting-started/views/messages-view-reference.md": { "sourceHash": "sha256:b4e921bba59577a9f8566439d26b22c2fa5e6dd7abc73ca5dee661d2d659c627", + "sourceBlob": "e329095be740023fce331f0510f7c95fe8e89bf0", "status": "translated" }, "getting-started/views/properties-view-reference.md": { "sourceHash": "sha256:6915dedc081501314e88a8e8700a946ec5217564c2b12e8aa45b3b4a54ad2a81", + "sourceBlob": "432f1db8f3f038c3b0d981ae84dad1717094ae1e", "status": "translated" }, "getting-started/views/table-preview-reference.md": { "sourceHash": "sha256:f010ef6c700738379e6cbf20325a39c7b86eafaa5728d59bd4adfd27b6db8dae", + "sourceBlob": "4a72949ba4c0d0afa312a7f732d48698f991d30c", "status": "translated" }, "getting-started/views/tom-explorer-view-reference.md": { "sourceHash": "sha256:e9d173f9dbc158abac089ab057372a1ef392b05a86c538c9e1dc931a0ff9f863", + "sourceBlob": "55e085d4cddf2da47e9dd983fd6576aa3a7b1da8", "status": "translated" }, "getting-started/views/user-interface-reference.md": { "sourceHash": "sha256:4d45273ca50773ebbcc8687abbda271ee00eef21559ad544f7c7e1c85875aac3", + "sourceBlob": "f57a8b263b7c1479670f1cbb394b304231ccfa17", "status": "translated" }, "how-tos/Advanced-Filtering-of-the-Explorer-Tree.md": { "sourceHash": "sha256:65c40ce723d4b32aff6b4591f2bbc7b92f9efcc4d8f5fee3beeeaf4faf58cd71", + "sourceBlob": "e9693aede33659c45ebc01810371f096a79a6d4b", "status": "translated" }, "how-tos/Advanced-Scripting.md": { "sourceHash": "sha256:c840e18a28dd83bd4d3d155855210b5b7f917cfcb41e3ff54fff7b9574015f85", + "sourceBlob": "55f6b13aa957f4249b43b1978d9bb7ddec2f13b1", "status": "translated" }, "how-tos/change-compatibility-mode.md": { "sourceHash": "sha256:3fbf4c15fa1139dceed32d02cc4c59e4d46c17e8bca55704057158be01f44300", + "sourceBlob": "82a38c6beda6c8d4f5e8afcef8f2ff733471447b", "status": "translated" }, "how-tos/connect-ssas.md": { "sourceHash": "sha256:ac0be330421f0ec892ea2f79efbfe6ab6115dbce8b663be873442f6d31266a3b", + "sourceBlob": "7ff28f3b00e7b2319aa3fab30c6b5f8e8ebabe84", "status": "translated" }, "how-tos/deploy-current-model.md": { "sourceHash": "sha256:678aaf20d780c1b7489968a3ddd0dc11553f102e5db763a0d545b49e368b5301", + "sourceBlob": "94011f3b3bc0dc6199c2042cc70b9b31cb094366", "status": "translated" }, "how-tos/drag-drop.md": { "sourceHash": "sha256:56d2daf749079cedf7edccdd1a6981c5cfc49aadfd04b91a2f7a95f481c2e3b9", + "sourceBlob": "86ead6b515aa5cf4f317413aa582e47c79afffa2", "status": "translated" }, "how-tos/duplicate-batchrename.md": { "sourceHash": "sha256:c0595217b896e03211f74b7fe747e6f6d9ca610669b0e08fe07ff459e2b39d20", + "sourceBlob": "ba58dd3f3f53b5bde0876bbd4a20833000bc5f0f", "status": "translated" }, "how-tos/edit-properties.md": { "sourceHash": "sha256:36d9f419afff9ce4d1ba814f1a97ec3c825e569dc0698c82d6ccec72380cbabd", + "sourceBlob": "84734a92e2f9f839920b9c909dc9ffc36ad46e98", "status": "translated" }, "how-tos/folder-serialization.md": { "sourceHash": "sha256:85959de08723f8acb20dc4f76220833d0d9fa9f71724fe1e68673c53041c0294", + "sourceBlob": "8a08bf0ee4a1a12987ff8f9bf646bf2e1e1952ab", "status": "translated" }, "how-tos/formula-fixup-dependencies.md": { "sourceHash": "sha256:f1b335d4a4c069ac21bdd9c20b0e34ed6442c63307f9d3a1cfdb29e4992daec5", + "sourceBlob": "b5231255f91b1d601fe3dde75e8334f3f243fb0e", "status": "translated" }, "how-tos/import-export-translations.md": { "sourceHash": "sha256:e360d9f6fb69f4ed660b150bc7cdbeaad188998f9addaca07a1ba95880158fbc", + "sourceBlob": "cf804376de4ed6cdbb410c91ad54adeb9ebab838", "status": "translated" }, "how-tos/importing-tables-from-excel.md": { "sourceHash": "sha256:6a68727673ad603d2091784ea68b0fd13ebc1eed7c5e279366841237e3319e9a", + "sourceBlob": "964498c7544e700c1a20e38f413c22a26c6a7d83", "status": "translated" }, "how-tos/Importing-Tables.md": { "sourceHash": "sha256:a4ec186a6a0cd94f851af6dc8a7277cad1216b3b63a0d973a7604774abe94f00", + "sourceBlob": "15052fae44f4edb632f91662e2ab026e5de5afe0", "status": "translated" }, "how-tos/includes/sample-metricview.md": { "sourceHash": "sha256:fccd99fc0bee912d39f879757f5b18d263bbe88d130d534fe5663283a2351dad", + "sourceBlob": "883819be7e347081b45543d64467304593e62fcd", "status": "translated" }, "how-tos/incremental-refresh2-h.md": { "sourceHash": "sha256:891d5b8bfa767c0bcfead8fa290e4f5d79116f9c95377fe1c6af51bcc56058f3", + "sourceBlob": "d0a1a2f6ff66142d05f782e5dfa959357c4556a9", "status": "translated" }, "how-tos/index.md": { "sourceHash": "sha256:7e51a733575206571c23bf168421d79bfc414e12300e2a04e23c98d5d1710a45", + "sourceBlob": "6202ed72237521668d52857ec6a7db2bfd038f65", "status": "translated" }, "how-tos/load-save.md": { "sourceHash": "sha256:4ac29ac34e1aee8edd2733d947e4519b26dbdef29ba4a372362a644d0488b1c9", + "sourceBlob": "4fa5c3c688bab68fddc3911549d7f0af864511e1", "status": "translated" }, "how-tos/Master-model-pattern.md": { "sourceHash": "sha256:b31a867bfe995adbb8ee02bdf5ff86f861752c6c98458f05b4b86db0980706a1", + "sourceBlob": "75ff116a81458138e84b66dcdcde2da7186bce43", "status": "translated" }, "how-tos/metadata-backup.md": { "sourceHash": "sha256:c61ea5c6dcce01019831e07daea29cee37a02e0dd5a05da7e71c6f42176893f8", + "sourceBlob": "035f1fb0cbc1cf94253b1a1e2e3cec50c4bc6bb9", "status": "translated" }, "how-tos/perspectives-translations.md": { "sourceHash": "sha256:65165835a80d24a358493e2af433e6d3833c039846e65fe9977c5d279d0f35f5", + "sourceBlob": "87113757d34a486c0a4e7d51004a110e50a499d4", "status": "translated" }, "how-tos/powerbi-xmla-pbix-workaround.md": { "sourceHash": "sha256:6ea66fe373bb9569fccaa28054c7841f07ffd88cd37196005e7a4fe2d78c4607", + "sourceBlob": "af4beb618c060fa28c0c3bdd3226ca732e716a6a", "status": "translated" }, "how-tos/replace-tables.md": { "sourceHash": "sha256:9e243b780ad11b7ed0d7bd28f34b7303a1de04f5c878123852e48aa69d5ffbf1", + "sourceBlob": "d3901ed42e65e427c92e58487f5354b3ae3476b7", "status": "translated" }, "how-tos/roles-rls.md": { "sourceHash": "sha256:252723e16bdc1e9eb192e6a7360e074ed8055c8ffa418d17b769b67b2cbb125d", + "sourceBlob": "393cfaa6cb11ecbe45faf578d8ff8d5cb5b9c328", "status": "translated" }, "how-tos/script-reference-objects.md": { "sourceHash": "sha256:f4807e41f2785b359bc6b9875519aaebdca84da06415bea5a6cd09179a59e18e", + "sourceBlob": "433ae78ee0d41f19ea256467943f17ba73976399", "status": "translated" }, "how-tos/scripting-add-clone-remove-objects.md": { "sourceHash": "sha256:4d28cbac2e46174af4ed28927c660275192fb013621c5b2d37c0dd5563f95f07", + "sourceBlob": "d5cfc7faafc9594c64b2ed690a8a6a99368ed8ee", "status": "translated" }, "how-tos/scripting-check-object-types.md": { "sourceHash": "sha256:e5fb737f603fb9234e8177ce7d888e6f73219b3823561bfea0d2db110e9788c0", + "sourceBlob": "c6188ad2a4fd831a36f46a90bbe5a0db795b4404", "status": "translated" }, "how-tos/scripting-custom-winforms-dialogs.md": { "sourceHash": "sha256:9830c8cea603b15203226db2df1e217209cc5b78e67c14fbbe11f9cbc1f229e5", + "sourceBlob": "805245219fd1e9eaadd2cf2a8a3fc1f6492def3d", "status": "translated" }, "how-tos/scripting-dynamic-linq-vs-csharp.md": { "sourceHash": "sha256:e989800b165f6c5b86076bbdb28e749cd7e882f7be7ee0f72f9e3924634abbe9", + "sourceBlob": "60bb8da7e18d3701169c1dd2881e061a6d9ea570", "status": "translated" }, "how-tos/scripting-filter-query-linq.md": { "sourceHash": "sha256:a269be822be81370fd10a982730d6eb1f47bc3ed2cc6081a28fd7b4428c632fc", + "sourceBlob": "d47a4170a72b9f4fc6bdcbd79738021c7689b1f8", "status": "translated" }, "how-tos/scripting-navigate-tom-hierarchy.md": { "sourceHash": "sha256:f12fd751688eab6151b40579ac6e2840657d4f8693364facbef5bcea131e9f8f", + "sourceBlob": "e614d0acf3fd92f519e2034521510c22c0532e8b", "status": "translated" }, "how-tos/scripting-perspectives-translations.md": { "sourceHash": "sha256:0767acd259fb03acba4804f0ce3f4f31d6fd03fb5313b000bcd7b292835ded09", + "sourceBlob": "3abafc41403b22cd846e2bd102ee389041d1f06e", "status": "translated" }, "how-tos/scripting-tom-interfaces.md": { "sourceHash": "sha256:73b571f60ab78fc67b7523f78e157a21127856a6570e53a9a101f344d2117f3c", + "sourceBlob": "ec584a1c307828a28e45d6e31475af68038ede89", "status": "translated" }, "how-tos/scripting-ui-helpers.md": { "sourceHash": "sha256:23b920a830d6bef0afa9fa6041fa3453824b50057121e5f4a6c2e92470fd00e9", + "sourceBlob": "4baf6400520710b715c0c1681b88b9b764a26b11", "status": "translated" }, "how-tos/scripting-use-selected-object.md": { "sourceHash": "sha256:430cfe77c38d7718c3908af2f7757e19b10fe2c1d5191c9113bc1c4c85fd286a", + "sourceBlob": "2b29d7f2bade0e62c41c8179b28cdb4854c8dcac", "status": "translated" }, "how-tos/scripting-work-with-annotations.md": { "sourceHash": "sha256:31f333c1fa02b5aa1b844b5e6b391f56f13b59fb4d3b6c0c4ef55d5a9a49673d", + "sourceBlob": "41280266752bb684f54960988e984871a4bdc929", "status": "translated" }, "how-tos/scripting-work-with-dependencies.md": { "sourceHash": "sha256:7d89dd667d39a7c6c397c6900e29897c28b1a8b2fdfcd815319928c1b1e79566", + "sourceBlob": "450322f96f5e3bb2f2682fb3d2ce182dfb44bc80", "status": "translated" }, "how-tos/scripting-work-with-expressions.md": { "sourceHash": "sha256:a294b1cad950289516b26454aa616a985abb037bff7f767fb17bb4ef56c77a3d", + "sourceBlob": "f0182324a9cba6c10727771deba327160e893c16", "status": "translated" }, "how-tos/semantic-bridge-add-object.md": { "sourceHash": "sha256:fc45b9109e248917012968eaed3d31ab2731c749be09ea3a3604317771dd6761", + "sourceBlob": "5853289794c1248e47cbd9436cf058a2fa7318ae", "status": "translated" }, "how-tos/semantic-bridge-how-tos.md": { "sourceHash": "sha256:3e7254cf0f1574d7a0b75109d152b36e4911d1ff417a4691b1bcafc115fd175f", + "sourceBlob": "dddea8d2f8d97917f10cc82789eba4912e00d7d7", "status": "translated" }, "how-tos/semantic-bridge-import.md": { "sourceHash": "sha256:5e44456f341235b172a178e01ac6c0ecfee7d3b6a4e4d955ba240a2ee4d0cb47", + "sourceBlob": "89dd79eccd4e906b2e55b9cbad4e08321db7d39f", "status": "translated" }, "how-tos/semantic-bridge-load-inspect.md": { "sourceHash": "sha256:5267a8486231514b4500cae234bf91aad00ecd2ccb5ae44389f46fb01b6a3de3", + "sourceBlob": "3be7731cef10e8d6b14c79b065acb008f37f9b94", "status": "translated" }, "how-tos/semantic-bridge-metric-view-handle-failures.md": { "sourceHash": "sha256:67ad4305aa673c9d258a13b33b0cfb7e00d23a22ab410f4abf197d80676a7a3b", + "sourceBlob": "b2b09c89151a48aa879766473e767e8d275fa9b5", "status": "translated" }, "how-tos/semantic-bridge-metric-view-import-from-file.md": { "sourceHash": "sha256:93070e6a840581a2c023da6990574598fba958968ef7c0c7fab22b489ae793e4", + "sourceBlob": "959f065d2b6233804002e1832992992588b3a01b", "status": "translated" }, "how-tos/semantic-bridge-remove-object.md": { "sourceHash": "sha256:4d3b3a064617e5b8792d448d6bcc6bfca71e449c7eb168e8958466096fae4fb9", + "sourceBlob": "1b936b008d16521e46b15e73b9665b3ac8e6ff89", "status": "translated" }, "how-tos/semantic-bridge-rename-objects.md": { "sourceHash": "sha256:888e6e749bc1d666eb315f3186540a12184bc538bfd73ebf97dbf68645c6092e", + "sourceBlob": "8d9a7f0614cca2bc9deb9f7d15040fd9b5ac7243", "status": "translated" }, "how-tos/semantic-bridge-serialize.md": { "sourceHash": "sha256:27ca90dfaf274d3d7933c2bb5839e2f6f28d213692e91b9e96d0b0199ac3174f", + "sourceBlob": "ac7f75806006d35a11fe820823ea3c908f23e61b", "status": "translated" }, "how-tos/semantic-bridge-validate-contextual-rules.md": { "sourceHash": "sha256:d29e2a536f0c2211f2f2a81896b281b994922d6ed8d82d6aa218fe2432c9b3e0", + "sourceBlob": "300d149c870455446d89f1a294f3889e69337624", "status": "translated" }, "how-tos/semantic-bridge-validate-default.md": { "sourceHash": "sha256:c580f957c8083de4a78ffd1b6c7c97801c7c39821d1362205f014f0d075e11cf", + "sourceBlob": "7739e9d5556f4f1270df2248041cbce5fa7a50e8", "status": "translated" }, "how-tos/semantic-bridge-validate-simple-rules.md": { "sourceHash": "sha256:8022c0d65d92617604199afbf23383d9a1f8ab74184565435f0f57b0d6cbd46e", + "sourceBlob": "18d275d46dfd61608e33c2d02367803da333d34c", "status": "translated" }, "how-tos/toc.md": { @@ -796,734 +991,917 @@ }, "how-tos/undo-redo.md": { "sourceHash": "sha256:857971043360cef22eb83ee113a0ebbe86160e1250ecfce378e7824e924fcc2d", + "sourceBlob": "a8f727796c265e8b4d78c6db70b0ce8cf14f858c", "status": "translated" }, "how-tos/update-compatibility-level.md": { "sourceHash": "sha256:ddde480e65570e65f073ce50cb6f366267f17185b5d7616b4392c5b2617c9808", + "sourceBlob": "7d0938f563bad097c57ca5c06dc9335db10f843b", "status": "translated" }, "how-tos/xmla-as-connectivity.md": { "sourceHash": "sha256:aa83da55214090f28678410499f5c8538315cab4035e722df4503c93b2bbf115", + "sourceBlob": "788ba0078f9be1ab9a58a19342277f6f903fee90", "status": "translated" }, "includes/ai-assisted.partial.md": { "sourceHash": "sha256:719add66234aa9744c77cf681f3f6dbc7d9c6d9eba02b4eede6698fd1915b2f4", + "sourceBlob": "9ceb370a56d4571df52acd9d014fd76df2f3609b", "status": "translated" }, "includes/feature-comparison.partial.md": { "sourceHash": "sha256:5eedcfa0c443e235ce6019a4301dff79f13d3d8df060985c702e1ea1807af6d7", + "sourceBlob": "ab3cde69a816175d12b841b20255ff4604dad348", "status": "translated" }, "index.md": { "sourceHash": "sha256:1418d8c61bcf18dd61bb8e62669fb07c7dee386efb23e817c1e27b923c173439", + "sourceBlob": "01783886195694d24057bf51a12805ba3d2d0784", "status": "translated" }, "kb/bpa-avoid-invalid-characters-descriptions.md": { "sourceHash": "sha256:348a0b21b9c03d6e496e239a00cfdc81d39610af0fd5ad6de6fe1ff623f7ceff", + "sourceBlob": "abd0bd25a0c7ab8cf3c87bcffa7352d2812d0e00", "status": "translated" }, "kb/bpa-avoid-invalid-characters-names.md": { "sourceHash": "sha256:8f013bf4faea84f9a24756e185ded303c9807c17b4ac5eb76df448b810e07eb0", + "sourceBlob": "be43497c2908b19810b925678e9b5f15ae8e6d97", "status": "translated" }, "kb/bpa-avoid-provider-partitions-structured.md": { "sourceHash": "sha256:86ec65070d81d5063e4d173d7c8c0966853f9b6694bbd8e726b468e7cb8b01f5", + "sourceBlob": "c0f3a440cee37381b8e7ab7e91dfefe61fffd768", "status": "translated" }, "kb/bpa-calculation-groups-no-items.md": { "sourceHash": "sha256:744c11cb3577957cc223454875f5ea95726fce0a94970698756f47b0a46c86b6", + "sourceBlob": "486293ab4b162ea7556bc08a19b913dff49f899b", "status": "translated" }, "kb/bpa-data-column-source.md": { "sourceHash": "sha256:a94e71eb040d34b57ad1f867e09460c23bf2d2e59ac848fdb92af82237851292", + "sourceBlob": "9bc9e55ba7c019f80f0640b5caf5350475144f57", "status": "translated" }, "kb/bpa-date-table-exists.md": { "sourceHash": "sha256:de3f26c0b6618516bdc57f3ea4575d35e22ff6dfe2adc856c77b6075b26842e1", + "sourceBlob": "97931246d12bdf2fea743e158eeb2dfa0297240a", "status": "translated" }, "kb/bpa-do-not-summarize-numeric.md": { "sourceHash": "sha256:244d8e116c98cab2126d5fc368471e4f432912e9e19181f58227d94b8322ddb6", + "sourceBlob": "f29ab822d626300b222279f055d74e13eebb8684", "status": "translated" }, "kb/bpa-expression-required.md": { "sourceHash": "sha256:8a411171214efbf1a8fd9da1be301ee6ca0f09bcadf38ce54082acf21984346b", + "sourceBlob": "5dd370ef5fd293e5964c5d79ffcdc21e790e7c39", "status": "translated" }, "kb/bpa-format-string-columns.md": { "sourceHash": "sha256:bf422113bd43cb69befbac033445b65b29a8569d9621a1c91eaf082a26047a9b", + "sourceBlob": "694283331456c549c1f1f5e49d83dba23f48995f", "status": "translated" }, "kb/bpa-format-string-measures.md": { "sourceHash": "sha256:bc6cd3c9bb8d66074e6f7836e4f2dd33f650df390bef17394e5cf385e744fce3", + "sourceBlob": "898dab2310a796ea3cfdd58a6bc40e683d6cb71e", "status": "translated" }, "kb/bpa-hide-foreign-keys.md": { "sourceHash": "sha256:9b67226f8b72ed7990a1c46dba893bcd36e775169fc562099bb3a8e17c47dace", + "sourceBlob": "62717d396eab4a7bee47dd8730c82c5baf2cd421", "status": "translated" }, "kb/bpa-many-to-many-single-direction.md": { "sourceHash": "sha256:b8948a747a93383b100ea910409eca99f2fc04e47360fc3e1210ba55b878d5d1", + "sourceBlob": "07454d86b7abeace0927cb1db8bf1198f5d3c1eb", "status": "translated" }, "kb/bpa-perspectives-no-objects.md": { "sourceHash": "sha256:8c2e025fa682be0443b0d926d49eef193f807af7d1df74736dcbe7d867140506", + "sourceBlob": "137725245254b98b6b61d183603a82e1c42f794f", "status": "translated" }, "kb/bpa-powerbi-latest-compatibility.md": { "sourceHash": "sha256:c4a79210717c4277ec658d7999fe9052678b865a199a22b563bdc9c21f576619", + "sourceBlob": "f05a1a043f32c3cd54cdf1dfd6c540728c0c40ed", "status": "translated" }, "kb/bpa-relationship-same-datatype.md": { "sourceHash": "sha256:291f46cc5c6ef8980aba3c960edd1e0d6d42de202f390d2ada4d4ec71342521f", + "sourceBlob": "fb3263fd4840fef07a75407f087d3f9f64913101", "status": "translated" }, "kb/bpa-remove-auto-date-table.md": { "sourceHash": "sha256:2328dbcb9c6571901c1f6565f73963c973556285e52765be67bf4d2efb3436e5", + "sourceBlob": "91451c32838492bbd5d08182d4aa8a6309db38d9", "status": "translated" }, "kb/bpa-remove-unused-data-sources.md": { "sourceHash": "sha256:58bd7d8482b7e17548d3b8f525c0f21c6648e2872815daadd8e5c1d84443f189", + "sourceBlob": "d5b82ed44dbc289e18de400acd317d125adddace", "status": "translated" }, "kb/bpa-set-isavailableinmdx-false.md": { "sourceHash": "sha256:9a5934a34ee1b4c911d855a78e87163a5427422d1ee59093b47d9c407f3bc50d", + "sourceBlob": "0ad2855c492b0a73efffd1987e3a49c52b8f1f7d", "status": "translated" }, "kb/bpa-set-isavailableinmdx-true-necessary.md": { "sourceHash": "sha256:9f1148b29e5e6ec221f24f0c4998e2da7d4a3955d23fe911dec5d63401e1094a", + "sourceBlob": "5c3062ccdc3100c2ccd3089749561a68acafe15d", "status": "translated" }, "kb/bpa-specify-application-name.md": { "sourceHash": "sha256:3de1a94d6a0b66003b65d5b06669ed905f792c3151cc36117abc73c10f52abcc", + "sourceBlob": "50dbf82bb1b645b52ff362db390f0936e03d7fba", "status": "translated" }, "kb/bpa-translate-descriptions.md": { "sourceHash": "sha256:8c73f97d93d7f0b5635b31d7e96bc5464f15f7cfc06507b8ca549edeae0cd374", + "sourceBlob": "24a01fb4145366af9c49fe2f02e50081ab03400d", "status": "translated" }, "kb/bpa-translate-display-folders.md": { "sourceHash": "sha256:10c7a4f9324e0de5c4c122ea7d10a2b6bb4e412a2f435802fc5e43d30dc65c53", + "sourceBlob": "a8b7bb3831ee01d1484896352d34d19171ed1b9d", "status": "translated" }, "kb/bpa-translate-hierarchy-levels.md": { "sourceHash": "sha256:27062bb6d7edfea8f0d78f3a789b985b18cc844690d29bc4127d55b66cac466d", + "sourceBlob": "63265dc258a5cfd66a0bd22537cb34a10566c298", "status": "translated" }, "kb/bpa-translate-perspectives.md": { "sourceHash": "sha256:8dbbdd6fd6f41cab313d51334c38cb91cdbb7830c560472a5abbdb39d40a7525", + "sourceBlob": "8a14a95448ccaa56b9e6943c1dbbca8f00594ada", "status": "translated" }, "kb/bpa-translate-visible-names.md": { "sourceHash": "sha256:48c6cc3b4998a6ca24f517a88b714f5e92c29add04010e2c607faca34c45d9da", + "sourceBlob": "db2d51f609860d881e46d51d9b762de7eb6ff3ae", "status": "translated" }, "kb/bpa-trim-object-names.md": { "sourceHash": "sha256:f9c4103682dd64f14587c68948264b3049aa3fcdb63997d7ebd3645075c4aa5a", + "sourceBlob": "ecfd33ebaf29ae5d8d69f6b4e73c111ed99631c2", "status": "translated" }, "kb/bpa-udf-use-compound-names.md": { "sourceHash": "sha256:6a626e28ba35b857383cbaf6069708880026531ad2dba4faee0a09cb52bb102e", + "sourceBlob": "062f09146b0fd25d65271d4695110f8e5a711b00", "status": "translated" }, "kb/bpa-visible-objects-no-description.md": { "sourceHash": "sha256:be6aff3d1618ad140b7be83ae59f97756572de2bc8b84eb5e035953f39642abb", + "sourceBlob": "975aaa1668ecf93ec148c560cd11e0bba47e3a16", "status": "translated" }, "kb/DI001.md": { "sourceHash": "sha256:0fdec75eb29be6ad908093cf6a95b9ab331bb568382772bf814fcc919a2dfc2a", + "sourceBlob": "d14d422967fbca7f07c93160b418e7e9ce09ca3f", "status": "translated" }, "kb/DI002.md": { "sourceHash": "sha256:068a81baf4c2c490ef06e51cfb9886723242dce676a9edea26e0948c0b00f702", + "sourceBlob": "7ef4489b2398ef807c81418dcead556913dd9fe0", "status": "translated" }, "kb/DI003.md": { "sourceHash": "sha256:b8e00a8116d08be012100d264b75fec171eedfc043a1bc52b1f2cf074c2dd0e1", + "sourceBlob": "5e334e1a8ab5dc88af4ac9e231d9317a09fdd8ec", "status": "translated" }, "kb/DI004.md": { "sourceHash": "sha256:0252909c58292e84696c2df9ce0ffb93452b96a823a684b06864f7e1d4b8fcec", + "sourceBlob": "c277438258ffa2d4288410951b88fd5a5a9c193b", "status": "translated" }, "kb/DI005.md": { "sourceHash": "sha256:8e03cd78771896db6b9dfb81a57e0a45f42335d6b242e0d474699da70843de53", + "sourceBlob": "a824b872fe91c257835ac593a6630bd0aae043e6", "status": "translated" }, "kb/DI006.md": { "sourceHash": "sha256:96086b12841c4fe8365a83fc91fb167c81d915b2c98e448489bdf55773167704", + "sourceBlob": "c211ed688d02976080723fd23e86a8b19171d3ad", "status": "translated" }, "kb/DI007.md": { "sourceHash": "sha256:78dd569b532ba7d5cb0efe9c96e5623604c5c9dadd67949d65ad14449832c1b7", + "sourceBlob": "e77da953e7178ebfc69524bc06db0afb6efe1d3b", "status": "translated" }, "kb/DI008.md": { "sourceHash": "sha256:5521d0162fdacf2da404c49d5715d5124f4fa682f793dce4df3d7b952519a2bc", + "sourceBlob": "6d9ffd07ea2419add3e5a9901f8e737916c81a35", "status": "translated" }, "kb/DI009.md": { "sourceHash": "sha256:790d74322a9d163c6acb9bf5d2b0e45352b85b954d68f2c1bddb06c4600361b3", + "sourceBlob": "7511b303c76658a947af3f720abf1f9750d282cf", "status": "translated" }, "kb/DI010.md": { "sourceHash": "sha256:bf5c57cf42365ee7869cb9f4d24c5ca20f96f79f73007a587b9034d25bb321df", + "sourceBlob": "5a96bc2247629a912bbed0e873743fd722fbc5d1", "status": "translated" }, "kb/DI011.md": { "sourceHash": "sha256:805dd2b2e2e754ef66ab07bba08fa0755c519e7432af9e83fbf4490a49f68924", + "sourceBlob": "34e89e0d3847256197cf309fd91bcaca9407942a", "status": "translated" }, "kb/DI012.md": { "sourceHash": "sha256:b9934cee7d4ea8c47cb70de48454918e64326fb8f76bee17db773b0f32627462", + "sourceBlob": "f90382e17bc4050f8cee1dc86a230c5e58a61a0a", "status": "translated" }, "kb/DI013.md": { "sourceHash": "sha256:19a2309ca26b9eedf30cde50cb70f3f70e8d8aa786ac98fc3e7bab74143d54e6", + "sourceBlob": "bdd5357797a7b753e584fded001ad022bc7e0aeb", "status": "translated" }, "kb/DI014.md": { "sourceHash": "sha256:be9a88c83f9dfa328b63298e7adf2a809b2cd83ea03cb14886838ac891536f1c", + "sourceBlob": "7a2b64d35cd0f94a45875abc87b98cdd97a3c8d7", "status": "translated" }, "kb/DI015.md": { "sourceHash": "sha256:c1c69cade3182935031f846cbe3f4165a4fd453ca77012e76f4f8cff1de2d3c3", + "sourceBlob": "bb8909be2c4ea9cf438c9fce99cb5cd9afeb7449", "status": "translated" }, "kb/DR001.md": { "sourceHash": "sha256:ad922ebf457863f55e34000fd78fc007c0d69f9bee556d2f79b5cd27c03273fa", + "sourceBlob": "567864910b021fdfca495800c76dec356ee776e8", "status": "translated" }, "kb/DR002.md": { "sourceHash": "sha256:0ad80ebc8cbf3ec27662dffc0210986cbabe8f92810bdb7a33717de8fab16037", + "sourceBlob": "edfa81ca29cd1af043dd6742a461098f183f3f6b", "status": "translated" }, "kb/DR003.md": { "sourceHash": "sha256:b76298c84d8c1269703dba2770d51a6674dfa4f03979e5aa71a53a456575bb49", + "sourceBlob": "8e6a5cd241342f8471369a4f436280446feb5468", "status": "translated" }, "kb/DR004.md": { "sourceHash": "sha256:d370d93822297f2614b44a30c04d330e5feef34160b080eb9ad3e019c70c7f6b", + "sourceBlob": "46db6e8307fa2f884b75663696d014d1c1b0d5f2", "status": "translated" }, "kb/DR005.md": { "sourceHash": "sha256:5b1451993738cd1b1737d47e96f328e5899f14028c161fe7c92357d74bfd29da", + "sourceBlob": "bec87ff60f07821e899b674b3887485c551fde5b", "status": "translated" }, "kb/DR006.md": { "sourceHash": "sha256:a91536c340fc07b1293e3a2e8cd4e89828d9fbdd8bacf3b23a736cfb8314e329", + "sourceBlob": "1f2da5f8e072439b8c6986d3c583de308be71006", "status": "translated" }, "kb/DR007.md": { "sourceHash": "sha256:a03a20219e9ed635de09831705034fe148912a694c50d8d36b87b4efa26ffbd9", + "sourceBlob": "5ef85825e6e5f4702156e2f5b8f74675a6020116", "status": "translated" }, "kb/DR008.md": { "sourceHash": "sha256:980a8345025fe79ac9cb8c3f841b204702f60dadb9574d72b33ba229cf9a475b", + "sourceBlob": "3eb0c734bc8cce8dc91d8eadc5e2480be52a2819", "status": "translated" }, "kb/DR009.md": { "sourceHash": "sha256:8609e608aaebfce4391671248f28d02870da055a99c90e126d56c4b415ceabb9", + "sourceBlob": "d909b86812cef9717b8fb0182f1349a7b1c8d920", "status": "translated" }, "kb/DR010.md": { "sourceHash": "sha256:76c8d124d90f77650b62cc3b03402177ef55f25898b032bc7b94180130778485", + "sourceBlob": "5de81896dba1404a890b2c047c6b80d30b63cfe3", "status": "translated" }, "kb/DR011.md": { "sourceHash": "sha256:ddb675bac420ba94a6a2eef2f7ab9cb4c9981b23398cc2a984a5cd30197743d2", + "sourceBlob": "9906b237f84815fc30ce12ce46e9658243eb510d", "status": "translated" }, "kb/DR012.md": { "sourceHash": "sha256:dc8c217235ef82b45ca02328f0e1d78ccc12114dddd5a44394126ec3cd638a61", + "sourceBlob": "28e69bf7eaeff3de9ab07e8a87481cc695eb391b", "status": "translated" }, "kb/DR013.md": { "sourceHash": "sha256:3e81af96332221c3b1f36655a6ee3f8dc0b116c019cdb7a5c26c5971c9cfc51f", + "sourceBlob": "7f3ef591449a24f94390f77301246fb1205f6102", "status": "translated" }, "kb/DR014.md": { "sourceHash": "sha256:d435c5959eb47b00a7aab3b846013228ec6d2077a3dc245e50c1a5328c392dbc", + "sourceBlob": "eea06f39e674a5efcb913329fff66538e282ac1c", "status": "translated" }, "kb/index.md": { "sourceHash": "sha256:8331987f38c8b52d8907a3f0d2c773d188e888cc823f2a5a21463d0a163f7a15", + "sourceBlob": "7306c1be29588cdb8e38fe55db3d5889dc7f0eb1", "status": "translated" }, "kb/RW001.md": { "sourceHash": "sha256:cc6cd9a2294f425380d825ff0a5ca882c3bd40a4d5207cf6cce0793f7c6af493", + "sourceBlob": "c08d64e9d73fdc1ca299b9b2e82e74238bea6baf", "status": "translated" }, "kb/RW002.md": { "sourceHash": "sha256:597595d6f8a1102114f51ae992bb519d21095f2a1003a8b1a90d3d96849916c2", + "sourceBlob": "f2e2d6b94caa5f166bf99fab7921d862b5b9e52e", "status": "translated" }, "kb/RW003.md": { "sourceHash": "sha256:289a445b62c13098e4ed6af7fa37901b1aec8bfb4d3b57e1e42eda8f1de9ad5e", + "sourceBlob": "c6cdbb64497acfd506e74ba67e4e56842a042b6f", "status": "translated" }, "kb/toc.md": { "sourceHash": "sha256:db0f8007f8bb5998ac188559ffcd9831d49a8c938f1b41434295625097a0a160", + "sourceBlob": "dd2c1e8d930ecabc9af854dd1b328d363be34697", "status": "translated" }, "references/accessibility.md": { "sourceHash": "sha256:2bca6bf296f3d236dfb29374577829002bf7dc9b42f6d5d29b8bb1e52be1691c", + "sourceBlob": "098c02fa53b19cb3e9ca766b8b15458bbec9b329", "status": "translated" }, "references/application-language.md": { "sourceHash": "sha256:3d8bb1e49bd7cf97c56210ab5c8322b7c419cbb0152966f195cabeabb87d1ded", + "sourceBlob": "c676538ed7467d3ba12ee644059cdb9856196175", "status": "translated" }, "references/downloads.md": { - "sourceHash": "sha256:d6f5f62ef64bfef5eab06ef77caad78ee695cd60e12114b7af9774a337063085", + "sourceHash": "sha256:27c21529bb0408933aedeb5614c99a4a2bc382554c45e4dd75e8f0fc45fa4281", + "sourceBlob": "551ccb2d3b4f9fe23823564d3f93b843bdc886ff", "status": "translated" }, "references/FAQ.md": { "sourceHash": "sha256:bf29800bdcc5ace4dd5d3813fc1fb8c48b30a034039a0285bd62768866b924e9", + "sourceBlob": "a52f303bdc0045ee8a8afa343a24e967ee09e141", "status": "translated" }, "references/FormatDax.md": { "sourceHash": "sha256:d6d07781fc5c8cbecb8187caa6123eb0a13f0f145abfa4b8c4cb1274c7e9dcf2", + "sourceBlob": "1f798e620c1b8873ff9948ce26655846070868f1", "status": "translated" }, "references/index.md": { "sourceHash": "sha256:f3f76163564f856590cabfa5c74ee31aadbdd37448f41b46bbd36358c3e60cef", + "sourceBlob": "9d07f1812f00c4ef6d531f0eb9a2467c52b265b7", "status": "translated" }, "references/Keyboard-Shortcuts2.md": { "sourceHash": "sha256:6dac63cca6fed07aeeaa4c9c97b9174104c299528f823785cb9d042fc567f93c", + "sourceBlob": "18406beb0667d006856d3e0ac2b616928730258f", "status": "translated" }, "references/object-properties/calculation-groups.md": { "sourceHash": "sha256:4f230c59f60e75119e22ffada811698effc81878640903305bfa17a506f28dc0", + "sourceBlob": "fc8fa1b2e9a6b5596e7eb4a7c5e2a91ed936465c", "status": "translated" }, "references/object-properties/calendars.md": { "sourceHash": "sha256:a649dea71fd889bb47c4778ea1b387e128dd16ba550fa99848eb12d4dbde7f03", + "sourceBlob": "22c32f600f5a47468e2451fe32644c6ac03518a7", "status": "translated" }, "references/object-properties/columns.md": { "sourceHash": "sha256:ad8d5949aeeb063589faa8dad1fc7ad32f7e5b4081901af99db80e2362654d6d", + "sourceBlob": "7f9de93c50705171f0ea1580c988c21ff4c23019", "status": "translated" }, "references/object-properties/common-properties.md": { "sourceHash": "sha256:99a20e619fbfc8aa59e91d3c78a0d8d6153b21d3c44275b0ec4edb3833a343d7", + "sourceBlob": "a85f44eef5b17a8ad1fcb3c911bd1a7d1a280b6c", "status": "translated" }, "references/object-properties/data-sources.md": { "sourceHash": "sha256:71c36ce369386fe8fbc5fc018e16fa30630605cf1e5fffac0e1d677bbb112bfe", + "sourceBlob": "df22b00cf3fa4a2da8379cb4ed75cd34575d7164", "status": "translated" }, "references/object-properties/functions.md": { "sourceHash": "sha256:16052cd3ae051a78d1b8723a2fc2cd07a675af2d4a75e4eb9adda44ebe2277ce", + "sourceBlob": "0c3c36de8e42b90be47ed1c98ff690fabf448265", "status": "translated" }, "references/object-properties/hierarchies.md": { "sourceHash": "sha256:801a8e2d3f529a5df6324a2449a2f10cb476d1d327c86b797e574f1bb767c64a", + "sourceBlob": "c17885494b4dcf220d189e16f14ea22585653083", "status": "translated" }, "references/object-properties/index.md": { "sourceHash": "sha256:f81a240dfc097835d1528695b989f4fc6f7ceb13f7820ba0510445a0ab92b8e2", + "sourceBlob": "777658c2bcd6eeac0aaf867a7068a1622f054d3c", "status": "translated" }, "references/object-properties/measures.md": { "sourceHash": "sha256:a56d044c177c1c788573e56c9c6c37d079b4b902a07ea3c2d4d1fc9a8c59c746", + "sourceBlob": "de6848a00e4ca9dd9831677207e9da83acc3900b", "status": "translated" }, "references/object-properties/model.md": { "sourceHash": "sha256:b101b856fcb7d2e9040b2fffa33d8fb712a65abbe20d37b332981031e1178bce", + "sourceBlob": "bd64cb58342640fb5998aaf98cf6fb50c5648728", "status": "translated" }, "references/object-properties/partitions.md": { "sourceHash": "sha256:77568919d2c1bb8e7ab595a3ceb5e763f07bb6720148e36e9fc45dd3b50991a1", + "sourceBlob": "99eb9dd8860b5e474841385803e43cd5e874ec3e", "status": "translated" }, "references/object-properties/perspectives-cultures.md": { "sourceHash": "sha256:ad263191d75b254fb88a0c2f3cbbf2e6eeb12f8bcdcc606c41ff059caebeca5a", + "sourceBlob": "02968a407205f76b5471f9e2ab140b43a619e5b6", "status": "translated" }, "references/object-properties/relationships.md": { "sourceHash": "sha256:99a40c5125f2607d9c5e341f2f2cd8eceebf6948da6f2bbf0c8294c49c079424", + "sourceBlob": "9fcfcd5ed6d436022bb124a7dfdc5c2f22951325", "status": "translated" }, "references/object-properties/security.md": { "sourceHash": "sha256:107036e38dcfb54408fdc842b3c188662b652bd10f8ccd43a1969caab93ab4a1", + "sourceBlob": "2b99f874ef26c9abce1b6c56533ee665e04b3e33", "status": "translated" }, "references/object-properties/tables.md": { "sourceHash": "sha256:ed8f9d50743d48e7e97e93a5b9008766228c89f8bab8fef4565f1399795f082f", + "sourceBlob": "ee2eb6cae06e4753b42282b1dc5e4aeb8cffb640", "status": "translated" }, "references/policies.md": { "sourceHash": "sha256:2caab8e0667baf27214a05cd1d8083570418378257a94a739c21abf6060ef5ac", + "sourceBlob": "3a1cb3d5d125926e4691709c8a5c7f3e6c5a70e9", "status": "translated" }, "references/preferences.md": { "sourceHash": "sha256:4b727d58249a9c4583c67d0dc40a32ebd45efe9e006f86cf404c53f094e8bcd9", + "sourceBlob": "561866a184e6faadce56d47bde51a32c274f1ec4", "status": "translated" }, "references/release-history.md": { - "sourceHash": "sha256:78226d0901a9debfe95e062f96697ba023c893852edcf6560dc89296382c0fc1", + "sourceHash": "sha256:3c28cf2db33088ef78f5af074d2c62d2678fc8a726768d62373c4e453ba16b96", + "sourceBlob": "c081f42cfb2bedc78a85dff2f7217b827b9946d9", "status": "translated" }, "references/release-notes/3_0_1.md": { "sourceHash": "sha256:9dd7d412075ffeb73fd903e6e560738125b944c1107d5279e06278b3a1c67ac6", + "sourceBlob": "eaa08f56a3be3673007e999c59ce874478c7861a", "status": "translated" }, "references/release-notes/3_0_10.md": { "sourceHash": "sha256:e4ed9e02f1d530888ea3291a800b799de6e5dce5717597aed5b02f96e6874819", + "sourceBlob": "4a09302e5a9863660c6f7ffb800ee39032ff7886", "status": "translated" }, "references/release-notes/3_0_2.md": { "sourceHash": "sha256:c259a508fc8aa8ab579248baa48edc26886cc11a7b46c1c300f5381f7f9a7576", + "sourceBlob": "640a8456627d2c06977b1b3d584ef2f8c33e4606", "status": "translated" }, "references/release-notes/3_0_3.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:e34eb6b6cc40a550c2b1e5555ab3dd5ae9aab4fe150191da8b217f7e1c44b7f8", + "sourceBlob": "e53733eec2e5cae03cc0d3aa97ebf5a3b520a61a", + "status": "translated" }, "references/release-notes/3_0_4.md": { "sourceHash": "sha256:0745acfac1154de9e0b61300052c871454382fff3120fa89b0bf5b794522bcb1", + "sourceBlob": "c5d3be45cca8e8823ee5bde6ed3cd399f12d8f68", "status": "translated" }, "references/release-notes/3_0_5.md": { "sourceHash": "sha256:0bc3323e03649c932b63cb6997eb73c33f7d8211601640a21630c7cb61851ffd", + "sourceBlob": "f6d6460df73b652bf239dfce67098268e441472e", "status": "translated" }, "references/release-notes/3_0_6.md": { "sourceHash": "sha256:1e570cc335e2d65c8c4a137009c2aedaa3aeb09e0a45140121b6bc7cbe1d7468", + "sourceBlob": "fdcd7af39cfac6d6f1a09575663b73491ed698df", "status": "translated" }, "references/release-notes/3_0_7.md": { "sourceHash": "sha256:5abefcdef1f490a553d1a625f1b3ae2c22e063ee14d751d483feeb1d347593bc", + "sourceBlob": "61e8539956e33e86ec42193f4a84e3b4caa253e8", "status": "translated" }, "references/release-notes/3_0_8.md": { "sourceHash": "sha256:cb1bfca1fe61a069fe1f3bf3f53ea69ba4209a25e434d94b0b1ff4fb63d64012", + "sourceBlob": "8ac0a6fd92d4cd375f72bee6aa2418ac7615b0e7", "status": "translated" }, "references/release-notes/3_0_9.md": { "sourceHash": "sha256:645ce98903aff7368af55a9b23fdeff5964c5804e16d3dfbbebe34f2eeaed37d", + "sourceBlob": "9826f4dd043afc8284fd6f83a323520be4ac95cc", "status": "translated" }, "references/release-notes/3_10_0.md": { "sourceHash": "sha256:280c2247e870b506a5134ad2da70ea21f53a5d7b2ad8e9f5319414e2f21d29b9", + "sourceBlob": "5ed8e34b1e58c79382f6d55d13fb01651504b6e1", "status": "translated" }, "references/release-notes/3_10_1.md": { "sourceHash": "sha256:8fcb4cee86daee9325723dc6cb6b9f3ffb3d9a41e67f0fc2c35510b8e0a78521", + "sourceBlob": "52b9d415dc4d1794ec1362fe2d6bae47a15de845", "status": "translated" }, "references/release-notes/3_11_0.md": { "sourceHash": "sha256:00487e4ef0ddf0ee9e93644ec740a1381bc984d716553cb60d2abc0140fc7b6d", + "sourceBlob": "79d8d94549a1157183b3d8946453fbc7f6878507", "status": "translated" }, "references/release-notes/3_12_0.md": { "sourceHash": "sha256:5ce41677cf81415a721c8db3c5c7e435c8b9c6fcf6ba61af7932cfcd2cd7f2b6", + "sourceBlob": "cdb6b3c2a3f6ff331a65c0f73641edd54b6e4b21", "status": "translated" }, "references/release-notes/3_12_1.md": { "sourceHash": "sha256:48c8104c99b0af6a26485ed1e19971ac9a06ca2fa143c55d1c2c5c74fe5252ca", + "sourceBlob": "be5152a46c06e8d0490a6911ec4982a0cd877eb7", "status": "translated" }, "references/release-notes/3_13_0.md": { "sourceHash": "sha256:43b9bff69d3f4fd23c2f98102be764674c6ee63c5832e8dccbf6cadcb2d5cb0a", + "sourceBlob": "258c28d26272a5841458961a7d47d2fafeb09fd4", "status": "translated" }, "references/release-notes/3_14_0.md": { "sourceHash": "sha256:63eea06882ec7b1838526b803fe22776fc3489a78e51a98acc3f4710212f5df9", + "sourceBlob": "157e5593dfd730dfd96767d6c0286cfc8ae67517", "status": "translated" }, "references/release-notes/3_15_0.md": { "sourceHash": "sha256:892511af91a5becc0cddf3c17031eb2772f1c0362f2e144fa6137a9cfef864e1", + "sourceBlob": "8efb40552718a548aa30d064a20715ffae67f825", "status": "translated" }, "references/release-notes/3_16_0.md": { "sourceHash": "sha256:c370d56ce763d64929dab9191dfec25d4d8e5cf4aac5ae242c965213497c07c9", + "sourceBlob": "3b266e6b9f7264325abfcf6dc129c2a2a801bdd2", "status": "translated" }, "references/release-notes/3_16_1.md": { "sourceHash": "sha256:8c963ee75f969d1f42991744d912d3ee9901209fe4729421791fff43cef34c16", + "sourceBlob": "beacd90074a0d632e9645d41bc34d3e6f85af898", "status": "translated" }, "references/release-notes/3_16_2.md": { "sourceHash": "sha256:2afcb4c569c945fcefcbb4ae5db451c3d3e6c386d6ce01520456af225cc397d7", + "sourceBlob": "5274249e21b216caf5da1f05c77b11ddc886aa7f", "status": "translated" }, "references/release-notes/3_17_0.md": { "sourceHash": "sha256:c8d747fa588ed2e9724ca5642094b3183a7848de106206c5acb7a6db003f03ad", + "sourceBlob": "e891fe862f50e87038435403460381e9c4275788", "status": "translated" }, "references/release-notes/3_17_1.md": { "sourceHash": "sha256:43426ca1856ba7146d873c25c365fd7ca9dd6c2dac27172d4302aa08caf73a13", + "sourceBlob": "6f2d2154cbdae6bb8b3bbdad7fa5e50c36f5cd03", "status": "translated" }, "references/release-notes/3_18_0.md": { "sourceHash": "sha256:ccef1d643cac9bbba5813f1f9101466c79a12f8adf5622f1b20780964bd39b36", + "sourceBlob": "c3672c4a50f8ab0a36e032f144a6b341b081f570", "status": "translated" }, "references/release-notes/3_18_1.md": { "sourceHash": "sha256:864335c116512c8d22394bc0b36873a8c4855d1a61a18f15524bd16afabb5869", + "sourceBlob": "2bc71e1f2460f5c30020657c7ee0e892a924116b", "status": "translated" }, "references/release-notes/3_18_2.md": { "sourceHash": "sha256:498e7482728a3b72acb8b42b8eec7caf732df56677d3b44ac0fbf5a32562ef17", + "sourceBlob": "9b95477e47178cc9d79e8465f48b606f2dd16fa8", "status": "translated" }, "references/release-notes/3_19_0.md": { "sourceHash": "sha256:1a50d8f5bbee16ffcefb119bcdc3f2ff8f0ee153f7f9848f0a4acd7383f8ac16", + "sourceBlob": "c10bbee907bbb2de24efeb97d8539df353765e0e", "status": "translated" }, "references/release-notes/3_1_0.md": { "sourceHash": "sha256:0585f4b1aad5d388d66147afa156d2e57e8f615ce5c3d3fb0830925d5393ee61", + "sourceBlob": "e68ce4c1162c5a60eae6649d8d24af01ca87b418", "status": "translated" }, "references/release-notes/3_1_1.md": { "sourceHash": "sha256:1d6e4b185d09f93a6688f14f954a766353fd3daa17ecedd6689735c2c01c5208", + "sourceBlob": "2b0eba0cec38d51b57cc59a652c0c18c84479590", "status": "translated" }, "references/release-notes/3_1_2.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:9f7ef93ca36c28f28b2a61b62b7cf68738da2a31a73abbc67ebacd4be6b05915", + "sourceBlob": "22f5fc643cfd26345fab9093675f28d7e622d952", + "status": "translated" }, "references/release-notes/3_1_3.md": { "sourceHash": "sha256:63da26ecf5029706dd4a732a7494b15ff4df4bbf3d4e1e7743f61b46d979d2ef", + "sourceBlob": "4e7945308676e1e8d5544cc425606e7682617054", "status": "translated" }, "references/release-notes/3_1_4.md": { "sourceHash": "sha256:690c2c05528da4f99adb4177a5e1ca448305c1093948d0cbb7948242961aa453", + "sourceBlob": "2f3c6c44bf305d44f19eaafb263980bfa693f1cf", "status": "translated" }, "references/release-notes/3_1_5.md": { "sourceHash": "sha256:c8984a8ab4c13021d98434ce71d4a54db9636dd3725c313de6795e2d6ad4fd88", + "sourceBlob": "c78926795d0da597a40ebe6031648e8327551efd", "status": "translated" }, "references/release-notes/3_1_6.md": { "sourceHash": "sha256:1e7e09b6f4d7b733050ee43873ffe842c7e55faba945cd9d5afce8996739f183", + "sourceBlob": "cc90b8afb55acd451b31b4c4e7f003f07d9f280f", "status": "translated" }, "references/release-notes/3_1_7.md": { "sourceHash": "sha256:cc33ce5dd956b6802c5184d84ead86749e676bddf9034b7228b0963ab1fa8279", + "sourceBlob": "6048b8c0f907deb1d015216badaaafd6c1d9ce09", "status": "translated" }, "references/release-notes/3_20_0.md": { "sourceHash": "sha256:5a259bbe3b2d0cb22234ba3fb754c0fc17f21a30ad3eea8af4090108d1103488", + "sourceBlob": "d4bbcf5383586b8a18c513795f6f746a4922493d", "status": "translated" }, "references/release-notes/3_20_1.md": { "sourceHash": "sha256:1adab99bfc877197b1138f98ae2889337b269b54951dbf8984609192eeb9f5ab", + "sourceBlob": "e3ebbc4d88731257564a1530a8f5cc8c32e3e85b", "status": "translated" }, "references/release-notes/3_21_0.md": { "sourceHash": "sha256:e989c0564fe64acbba6215cf25eacb1e79c6da59f17756ad9ea74500a059dc8e", + "sourceBlob": "f537e5ccbb04821871aa22107ce6f330e4e2c473", "status": "translated" }, "references/release-notes/3_22_0.md": { "sourceHash": "sha256:71625baf4598ce69ea8022cde2848d04b60229d8c60ad15bb9fb806540d62288", + "sourceBlob": "a60ffdea08da101e2afda4254b4af15effb05abc", "status": "translated" }, "references/release-notes/3_22_1.md": { "sourceHash": "sha256:1e3e64b4b3955f35edd2a9c0bae68ca092be3729c7cf46ee376234ef1f8432f8", + "sourceBlob": "311e34b247c68c60aa04d03ee1eadd4906d85b33", "status": "translated" }, "references/release-notes/3_23_0.md": { "sourceHash": "sha256:5dfe7273a83bb750fb67556bfaa0494f3474489e8584d8a4416f81db7638e4b6", + "sourceBlob": "b9930200f2cdc46ff68369cf4a8df41d8c967c24", "status": "translated" }, "references/release-notes/3_23_1.md": { "sourceHash": "sha256:a9693c900f2bd1ba870cbd36bbd3dae8c5b2cc07046953c0f1c62f8ab073b41a", + "sourceBlob": "416f497125bbdd7993af9c127b4391a17cbfeb4a", "status": "translated" }, "references/release-notes/3_24_0.md": { "sourceHash": "sha256:7e06a6582ce450f85e58773fa486f8dfa2b57ae5ff8ba5228e90a2df42b2b0a2", + "sourceBlob": "694dd28542b426a34078f0ebb7bc97cacd16fc49", "status": "translated" }, "references/release-notes/3_24_1.md": { "sourceHash": "sha256:2f6edfbfe6bac0cf0c6e0ca0c941208626b1ae2df232c8dc14046a2c6312fd9a", + "sourceBlob": "d80c0787b330d18c3b56d8dc90a1d890dc225206", "status": "translated" }, "references/release-notes/3_24_2.md": { "sourceHash": "sha256:002bd955620438ac8a76221d201bf17027262f15b5356fd994a340dae2953c46", + "sourceBlob": "cd937eed1367167e0f9719edf02728a0972e2144", "status": "translated" }, "references/release-notes/3_25_0.md": { "sourceHash": "sha256:94073554f6e49579ce01eeb1ad53b54d66d8a2949d9fb2281123688b597594b1", + "sourceBlob": "da844f0a8b78dae1af06c1e44eca177bb7073b88", "status": "translated" }, "references/release-notes/3_25_1.md": { "sourceHash": "sha256:5327769633fab3d45273026fa50388e6cc572bc4e3ad4e1685ae544d777d96be", + "sourceBlob": "40ba9e52b8d1b93c4013f9eb37f28debe2c7e1ca", "status": "translated" }, "references/release-notes/3_25_2.md": { "sourceHash": "sha256:ed0cfa9bb38b492a133d7742ffb1a57169616ea1f604ef3091d0dba292a5d9c5", + "sourceBlob": "ccad60192197d0e3ec62815a58c1600d5c4e70d4", "status": "translated" }, "references/release-notes/3_25_3.md": { "sourceHash": "sha256:e2aedc2e010c287c6a6480f950eb5abaa06c24faeec99b6f298ca9236e8ecc11", + "sourceBlob": "ec4113652b784b9140527d2ab367da6d817b49a7", "status": "translated" }, "references/release-notes/3_25_5.md": { "sourceHash": "sha256:cbceecff8d36b2bb9f4ba3a54c9957fd081f06ec869f2d37eaa14bd731854744", + "sourceBlob": "226f19b5290f99a8ff8655dc203246a8937706af", "status": "translated" }, "references/release-notes/3_26_0.md": { "sourceHash": "sha256:877aaef72b1b712c4ac6060a0812b8f6a37e85b5d7f7c907d73aaf0aec50b4c0", + "sourceBlob": "fa3c637eb355e008f8f7d9912f8de2561524de6e", "status": "translated" }, "references/release-notes/3_26_1.md": { "sourceHash": "sha256:454c83599c5098dc644a438bdf15a80714a45f53fb63ac17d8514b1b197050c8", + "sourceBlob": "65858fc9481c6ba565ccfe93caacf7ffe7ccceab", "status": "translated" }, "references/release-notes/3_26_2.md": { "sourceHash": "sha256:fa204a34a64c2f6a9e2764031e5c0c255b684edd6cbe18fd195a0d4fea2978bd", + "sourceBlob": "2930da424bbf6971360b78779a1eb08f110615ad", "status": "translated" }, "references/release-notes/3_26_3.md": { "sourceHash": "sha256:feb01ccd22d33c9adb705a0b7f6ef3523f4dfbdf1905410809d6b3f193515335", + "sourceBlob": "dbb47faa35f8d709fad409c4e74872e84b2d601a", "status": "translated" }, "references/release-notes/3_27_0_0.md": { "sourceHash": "sha256:0871275249d3bbd3ca332855873c89ed84a476929a2216333724fd61a05bdfc3", + "sourceBlob": "68e25c54d913b88c1582ce16441a2ada3f2fdf1c", "status": "translated" }, "references/release-notes/3_2_0.md": { "sourceHash": "sha256:f80a34035dde22a0619af4ce18d51f59866d2eff27418c893e676f9da877b10c", + "sourceBlob": "a41c0dd3aaa4a476f51d438c306923247438202f", "status": "translated" }, "references/release-notes/3_2_1.md": { "sourceHash": "sha256:43e1be32e10f7a17a7d86df369eef21a78ebe5b2a52b53a1f83b6fd386b29049", + "sourceBlob": "c2517496a26e4b6dc7c78b2badbb770be10cdb03", "status": "translated" }, "references/release-notes/3_2_2.md": { "sourceHash": "sha256:2da5802638de5f4ebb2f12cffb33be191351e460ce411293e865bf8310ea9747", + "sourceBlob": "62caaf92592bee009ac8060d70ccbdced3d09cd6", "status": "translated" }, "references/release-notes/3_2_3.md": { "sourceHash": "sha256:233ad017abfdf9b6cf5938bcd58d76dfac5aaa83c691e6b8613e8501ead44f6e", + "sourceBlob": "959af24862725fb1a013f758fd5f2ba904a5cd43", "status": "translated" }, "references/release-notes/3_3_0.md": { "sourceHash": "sha256:6407130a08fc4aba3742cdc01f6fcdbe315036324a6ef4ef485750bbab90d75e", + "sourceBlob": "39e8bca64ac28021e204576aafacc224d3ec1a17", "status": "translated" }, "references/release-notes/3_3_1.md": { "sourceHash": "sha256:069220239a8a82d1f0d310a468b3af4959525d5b03e11298fb2049e0ebacc52a", + "sourceBlob": "c3aa53c9e8ddecbbcf98afa66cd6b91ec3c07ccc", "status": "translated" }, "references/release-notes/3_3_2.md": { "sourceHash": "sha256:e11afa6e505c7d0d7a2cc19b12232f05b12a402d1534d689a3987a692ad6eabd", + "sourceBlob": "e629c7a0a7a9bea0c8e21dd5bcd359e35b6d50fc", "status": "translated" }, "references/release-notes/3_3_3.md": { "sourceHash": "sha256:b25a3e45ea9245ff2348aef322932081aa9419469de741bd98ab45435fc01106", + "sourceBlob": "18c520088587fa1a866707d98898cb9d84cd40bc", "status": "translated" }, "references/release-notes/3_3_4.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:dabac3bc52ea3ecb59ffc39e10a13d252a29530412a604c284cf33fa988e7ccb", + "sourceBlob": "44b1704c61493dbce2d4bc1ab2dc690fcc6f9282", + "status": "translated" }, "references/release-notes/3_3_5.md": { "sourceHash": "sha256:b88d6bc31375caca6ccbc5af1f95d1c64a64bac5d5861d540a5a111046ec2709", + "sourceBlob": "53e8a962a21b59d14df3ae2f160fdfb2aca78f72", "status": "translated" }, "references/release-notes/3_3_6.md": { "sourceHash": "sha256:2eea5eec39317d1fc007cffd6971bf08411e04b79984bac9e9e7c5f4a9237329", + "sourceBlob": "00dd2df7edb17e10f6ed795ff534113f1c4f12e2", "status": "translated" }, "references/release-notes/3_4_0.md": { "sourceHash": "sha256:45921e73217c3fd5d69b614e10428aa428b0d466bdb49d83058cb79a49b846c7", + "sourceBlob": "2199967ef076f80633a7fe3eeed70b2087c0ded2", "status": "translated" }, "references/release-notes/3_4_1.md": { "sourceHash": "sha256:2c4222b3083566a37505c7ede112ee3076111acf89a9cacb7335241c5195a1be", + "sourceBlob": "3fccb82edbf6e990dd82a79bedd88d1dbadc5c20", "status": "translated" }, "references/release-notes/3_4_2.md": { "sourceHash": "sha256:8aca2f841be6c495daf791f1c8a812605c4215639dd6d61bf9a99a1450913eb3", + "sourceBlob": "9968216781396ba6765dd9d0bed794e7890e1246", "status": "translated" }, "references/release-notes/3_5_0.md": { "sourceHash": "sha256:3e8574e571c37b8fdf644f42377a8f20e4494f57683aa2aeeb66f6cc5dbb3b3d", + "sourceBlob": "881c8d306d8e72265e29483ee7c1d2af573d3eb4", "status": "translated" }, "references/release-notes/3_5_1.md": { "sourceHash": "sha256:bbf6c0a5eaf0581ef564c23861341ba086a336a7a0c8556b18c82a3b97263de0", + "sourceBlob": "6d9a4869705ced67dfbf0b772934ef63f0f3ce04", "status": "translated" }, "references/release-notes/3_6_0.md": { "sourceHash": "sha256:390ff39fcddb43462ce6371e434faeffaec2542bf1ce3871bf109c31afbebb8e", + "sourceBlob": "d9dae63614340c0f7646dd950e14bb4461886ad6", "status": "translated" }, "references/release-notes/3_7_0.md": { "sourceHash": "sha256:1193e309b3e6b7c5ab312cdfac506a029c5d3fe1a46e55c44cfa73ac02b05559", + "sourceBlob": "a398780ba9c12f6fdc6530452736d12834c5b6fe", "status": "translated" }, "references/release-notes/3_7_1.md": { "sourceHash": "sha256:bb30b3f2c2edfb66b5b948a092b63522ffae1661d74837bec19ab8df42931536", + "sourceBlob": "d16329f471e3337ac0299c5b5459a52eefefbcaf", "status": "translated" }, "references/release-notes/3_8_0.md": { "sourceHash": "sha256:59c7b7c50555df06c3b4d8862429c9dac365f71abe0b2867433c4eb467ff227a", + "sourceBlob": "a1a261976822443a76a2ddac1c1407c9ef336916", "status": "translated" }, "references/release-notes/3_9_0.md": { "sourceHash": "sha256:435580f98c2577516a67e1c12f4c451c0f3116bc864b4dc1f474b47650d048c1", + "sourceBlob": "abc613201bffe9a8516062396010f5e4bc93d91b", "status": "translated" }, "references/release-notes/beta-16_6.md": { "sourceHash": "sha256:cb20f9fb04a9bd84b4354ccda93c724199827bd1bed1afde6704ee1c3515ad3a", + "sourceBlob": "5d83f9615f5671ccc033e54def9f496824f7e89b", "status": "translated" }, "references/release-notes/beta-17_4.md": { "sourceHash": "sha256:4932a0f3a3c1fa9f5691ff457063f111c3c44c7aab43709d18118d5e2214ad1d", + "sourceBlob": "84166e224a046502b9156d8fd7432da52ca37969", "status": "translated" }, "references/release-notes/beta-18_1.md": { "sourceHash": "sha256:948893b0e25d9c457f7757c8c2aed0be34f9fd7897e47dfbb9d48adf34281a9c", + "sourceBlob": "60f0d87cdf4bcf21a86bbc3bed6b335a208752e9", "status": "translated" }, "references/release-notes/beta-18_2.md": { "sourceHash": "sha256:44023c249d6312fe7174132138a20bee776cbaac990f9f63be3609f405cf7ecd", + "sourceBlob": "bbb976acab63bafeb87f8223519c3f7cbc45d027", "status": "translated" }, "references/release-notes/beta-18_3.md": { "sourceHash": "sha256:38d2a3684c5f3f64f6e3ae40109694ce34f5f45c7fa7105e5f4c627377faa3dd", + "sourceBlob": "964dad71690f090a70f2654078c6db72849024b0", "status": "translated" }, "references/release-notes/beta-18_4.md": { "sourceHash": "sha256:f4ab4735ebd2b80d7d6539b8f18065c5588753343112677f54e687d057bbb06b", + "sourceBlob": "c46b3385bec532838592f34578f2c176af2176c6", "status": "translated" }, "references/release-notes/beta-18_5.md": { "sourceHash": "sha256:14d1a1cc25b99f4ecbdfa2e51bd73dae43a6e3dd6536909cb6c7ca610cc84938", + "sourceBlob": "9a4cd435e8034c82e0e96aa493c3640811bc0336", "status": "translated" }, "references/roadmap.md": { "sourceHash": "sha256:b0995af14ffeba3c104b4bcc0d65bfedea42ef4e3ddeeed9e8d2d1f594efc0ef", + "sourceBlob": "30feed902ef620fa0946591f63b1da3f6656335b", "status": "translated" }, "references/Roadmap2-h.md": { "sourceHash": "sha256:91b4c2b6a63a4da62e8cd187dd8fa50e9ed7f8e4344f3a7a509dab9c228850ee", + "sourceBlob": "efd79f7499c27eb979d26e8ce014dd21391f61b2", "status": "translated" }, "references/shortcuts3.md": { "sourceHash": "sha256:01a0120df7bf52cc07963c89f687b30e92c8db68d0c7e529fb00d1ff352c84fc", + "sourceBlob": "8f3b3e1887a6e44f18593720504d454d46265c47", "status": "translated" }, "references/SQL-Server-2017-support-h.md": { "sourceHash": "sha256:91cbe97b5d1fcf552a19c198232c12a472abecf0d5ff75eb7b75b737e00598dd", + "sourceBlob": "b328f1840a4813301d1106fc411185bc3f1f080d", "status": "translated" }, "references/supported-files.md": { "sourceHash": "sha256:b914a46e482200adc4b407c54a40cdc4312d5698ff765fc661d83c4b0333b60a", + "sourceBlob": "384658d9b7607b8ea51de37c08f9e4db022514a3", "status": "translated" }, "references/system-requirements.md": { "sourceHash": "sha256:1ca5de3bd3440c895a05b276f9bf93d2a5fe80092b93928712c99994712827d1", + "sourceBlob": "97e7d12c6dc64528f758362a7c052197abc0aac9", "status": "translated" }, "references/TabularEditor.TOMWrapper-h.md": { "sourceHash": "sha256:d1ed449a9a96c0031fe8b280f1c808858196806f71363d8fa9447e0db6e7d71a", + "sourceBlob": "d394d08152bc4c7c5cb5d67b7c2f2863d0830a8d", "status": "translated" }, "references/toc.md": { @@ -1531,35 +1909,43 @@ "status": "untranslated" }, "references/user-options.md": { - "sourceHash": "sha256:ad092f6088edad3d3e8bd124c227f618338b081a453118946880a2fdc8dccf9d", + "sourceHash": "sha256:c1c940b4529b30a1be9a016f78877c647519695975505908b58abf3ce615ed86", + "sourceBlob": "36936949aa34a7fff8e4fb07abe2ad5ccdc6782c", "status": "translated" }, "references/user-settings-files-te2.md": { "sourceHash": "sha256:0a2da6cad41a5e1983662c6d621ab9f78f671aa59f2389f31004b7847deef1f9", + "sourceBlob": "74cf282fbaa6ceb6103de733d816c5dc3adac328", "status": "translated" }, "security/ai-compliance.md": { "sourceHash": "sha256:4c6a93c738e015fe6143730853cffa714e10e80d7911eb6ff5f749dda7a364ad", + "sourceBlob": "5a4956bb3144397fac8b53cbfbd5d66a90e4c6ed", "status": "translated" }, "security/gdpr-delete.md": { "sourceHash": "sha256:25dafde7519c658a7212697ee19bee0033405761a2dfda883549908d63e7ea1f", + "sourceBlob": "202e91437a2e1c53eadf1921e8bdc18bbd716a50", "status": "translated" }, "security/index.md": { "sourceHash": "sha256:77a2eb1494aca34dcd4d36cecd7b1aff207a12ce53e149f69626da621fbecbc1", + "sourceBlob": "a1dbdc12c24f7ab974e64768956f99a48463acac", "status": "translated" }, "security/privacy-policy.md": { "sourceHash": "sha256:3b3d626bc3e999fb30369deca46398b5799282797bf93325e4566fab677fa59b", + "sourceBlob": "35e671a05751e7f6c0a36d57d229a0ada85ca450", "status": "translated" }, "security/security-privacy.md": { - "sourceHash": "", - "status": "untranslated" + "sourceHash": "sha256:5d9f2e13cdc658c57cd7a23bcc73bedef02662c7efa62cb1bf1e780e53b281cd", + "sourceBlob": "fb50e726397e3e9f565b8aaecca3838cca6676f1", + "status": "translated" }, "security/terms.md": { "sourceHash": "sha256:90fd51f0cff45e9e16b2c200fc4dc3ccf769b97a3c6afcd09b7a688cfc5b87f0", + "sourceBlob": "ca19992dfe8fa89ca508cbc83c172609d6ab67da", "status": "translated" }, "security/third-party-notices.md": { @@ -1572,62 +1958,77 @@ }, "toc.yml": { "sourceHash": "sha256:11d5df2671e45efed1baf56cd10c6634fc48f5f34b8f40d5b83456258a929108", + "sourceBlob": "b6b79185c71b3a28258ab0af6bd97814d453f14d", "status": "translated" }, "todo/as-cicd.md": { "sourceHash": "sha256:b9c4e2179d5be3177a2119ec147214c8f0e5f19bc82993fce18a1712717ad839", + "sourceBlob": "a6353f134980788549d9de1b2b48c4ccfd06a63f", "status": "translated" }, "todo/Maintaining-Calculations-using-Scripting.md": { "sourceHash": "sha256:f01409f53e2dd517b8504f021d56725efc821bff3e94d5cf25c223f6ee9ed977", + "sourceBlob": "fa43c779e2eff353ce81dd663a8bc2b0cddfdab2", "status": "translated" }, "todo/powerbi-cicd.md": { "sourceHash": "sha256:a564d8b5f8312289b739fa332d0dfae6a3b07822e454da25c6f10c16b1e4c016", + "sourceBlob": "04266dfb17a20c5e49c1a9678ab1b1bf2c41d03d", "status": "translated" }, "todo/te2-advanced.md": { "sourceHash": "sha256:02b6a9bfe2f57d4a8ca9cca47d635343f4dee8b1759807de26faa9ffd51157f2", + "sourceBlob": "7cd22aed96cb2475edbf32cfbdca286c94976d3f", "status": "translated" }, "troubleshooting/azure-openai-connection-errors.md": { "sourceHash": "sha256:e0212bb587e9af4f18ba63af831a01f9e84b893a8306685475e6a55abbef8650", + "sourceBlob": "62dc40b0bda20817c22a73347c263292c141db42", "status": "translated" }, "troubleshooting/calendar-blank-value.md": { "sourceHash": "sha256:0306783580e503f6745682cf15906a60703d87b56ce743eb0f4e04c6b2ed4ca9", + "sourceBlob": "0441b2d4193984196a71087459cbe073fe63a547", "status": "translated" }, "troubleshooting/composite-model-measure-formatting.md": { "sourceHash": "sha256:d9bc88bd4f53f3ede1eb636f1e9815e87abb0b3d6c2d10a68b4af5fb8dbed0fa", + "sourceBlob": "c1beaaa918f415a845e3dfe48c94f4c0ab902183", "status": "translated" }, "troubleshooting/databricks-column-comments-length.md": { "sourceHash": "sha256:5fb9b67014b00e5af4fe732edd4c5b9c99d00adbe3e251f3213ab4d17f2751d5", + "sourceBlob": "8384ed4116a6b07a8705ec2ea293f10a8410f9b3", "status": "translated" }, "troubleshooting/databricks-refresh-empty-catalog.md": { "sourceHash": "sha256:156740404acee951b2e723d390472cc2fed4ec46603eae94c6a8026baf503681", + "sourceBlob": "eee873bfcfd48bf75f4d41fe2959ef9854c9007e", "status": "translated" }, "troubleshooting/direct-lake-entity-updates-reverting.md": { "sourceHash": "sha256:aab510b71ae2d9128c2c1fc565e2e3a3bee5cf2e568fc57aa33a8710b1f650cc", + "sourceBlob": "164e251bce171b4e59ff7a4fe13b4d6d3bf1502d", "status": "translated" }, "troubleshooting/index.md": { "sourceHash": "sha256:201e48fac8aefa81c9dbcf119fc81fc8783ecc822ec5bcf4dd6fc464b8c15393", + "sourceBlob": "a7a225176ee2b15c7abaf6c3e3747e14f8dd92df", "status": "translated" }, "troubleshooting/licensing-activation.md": { "sourceHash": "sha256:1c17fa9f1b9589f06b54c3acd3fa2edc4cb5ebfd428ea71a2dc6e5fb4ab6ac21", + "sourceBlob": "9ad3a2f6b18585fa53faa4b5f52ac3ef986e186a", "status": "translated" }, "troubleshooting/locale-not-supported.md": { "sourceHash": "sha256:3913bccc1fa6aafb9d2d6ab07f4018ea91efa1c1cc34874fc7267ab9b7034091", + "sourceBlob": "aa852b2bcea0af960f5c3c65808971985a321e93", "status": "translated" }, "troubleshooting/proxy-settings.md": { "sourceHash": "sha256:9de7a8f714d448d4dd5184645833cd2a0ca298d2743cae52595ef2ebb83044c8", + "sourceBlob": "a3a5d486c7a589e36b20bada23744006afe59a37", "status": "translated" }, "troubleshooting/toc.md": { @@ -1636,94 +2037,117 @@ }, "tutorials/calendars.md": { "sourceHash": "sha256:42d5fbcac1800e22943c402afc8cc0ab45ecae62d802ffafb492d37a3e4096b2", + "sourceBlob": "dbf139ed2a2573cf48db4ba4254b24295c441f65", "status": "translated" }, "tutorials/connecting-to-azure-databricks.md": { "sourceHash": "sha256:6a47b35772c99b233424bab2318b397f1d866227cc45646e0d11e9c89c4bc9f3", + "sourceBlob": "79fba8437efa8fbd95c8eebf9d72cd774515dbf0", "status": "translated" }, "tutorials/creating-macros.md": { "sourceHash": "sha256:c6c5951e9b76a66c146878235441b06c929595c6d11e86e2a23f8bc00eb5e9d4", + "sourceBlob": "38d8c3225e65db262182e2276a5bfc9acfe66b10", "status": "translated" }, "tutorials/data-security/data-security-about.md": { "sourceHash": "sha256:4a0551c3deb39c692e470f6ea653b5c125c19ab1bad13b81cc117569c5fa20ac", + "sourceBlob": "406353ef6b621c24e83234e5e1b3ee457ccc2139", "status": "translated" }, "tutorials/data-security/data-security-setup-ols.md": { "sourceHash": "sha256:c2f2266fc6f918d57da5fc9347a5c21aee8b769c020eabca32b0fc4408605cf1", + "sourceBlob": "a10baecb1c42450b6792e7dadfd4212e44988bcd", "status": "translated" }, "tutorials/data-security/data-security-setup-rls.md": { "sourceHash": "sha256:af1e2cdd021ffc9ba788cc24eceae8ec1cdca3d4569adaf0b8f271e7e5607536", + "sourceBlob": "2550f485caa3f2ef7292f6a1a4f481bc8f364cfe", "status": "translated" }, "tutorials/data-security/data-security-testing.md": { "sourceHash": "sha256:99fb2630c22ce822b6247189570559ac4bd907a5e19a2b9698ccaf7e7f55a537", + "sourceBlob": "41360870728dbf7d73b6be5c1d2d7eb4f4cd7d8a", "status": "translated" }, "tutorials/detail-rows-expression.md": { "sourceHash": "sha256:db88c79f625f34221cf230c18f11fad62c76fdd79e8a191e54eecfab82156180", + "sourceBlob": "c5b7e3e8450333a7019e1d3826d5b0b5a16e7af2", "status": "translated" }, "tutorials/direct-lake-guidance.md": { "sourceHash": "sha256:964fbdf58e990aee599a3a27f26d5b302cafd8bf85c950a79eebb84f497f1a2d", + "sourceBlob": "a87af70d967a54fb3b1a6c89ce4844609ab99260", "status": "translated" }, "tutorials/importing-tables.md": { "sourceHash": "sha256:1e9205918c6b3e8b7b93dd6c47efe358059e49876878db2233a25a8476257d1c", + "sourceBlob": "ca1a1572338523fded0c48ae9a08d3628911f25d", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-about.md": { "sourceHash": "sha256:4ca05cb2809b0aed7ba334a9f4b6c9ccb053658b4c1cc42dc29562b74cfad42d", + "sourceBlob": "e76ceb0faedf1fcce7bb3d21a621f79ed8f04680", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-modify.md": { "sourceHash": "sha256:9956a6e4fc473c10829c0fcef6ac14a1e569fccd0cfd84d6f3b2cf6fba57c9a4", + "sourceBlob": "087f5c46eb42f5235964b9fb57f9b00742727545", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-schema.md": { "sourceHash": "sha256:73e8125efd00942a78f5931bbe9f0e440742ee8d6de6f54e43a87c992a0ef5ff", + "sourceBlob": "449a21b482c4f9e586a125cee72f121c3e4cc48f", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-setup.md": { "sourceHash": "sha256:9b8100d72880bf60b310d4090727ff8ce66c7da042b85c5558754a66d6508739", + "sourceBlob": "9db8313ca6d20829cb7bc4ab88de6e58aa663c77", "status": "translated" }, "tutorials/incremental-refresh/incremental-refresh-workspace-mode.md": { "sourceHash": "sha256:8afd82adf5b708dbfd8dc241ca03080817cf808960ba29baf606295334893209", + "sourceBlob": "5596ac89dd389793101181495685a43543666b35", "status": "translated" }, "tutorials/index.md": { "sourceHash": "sha256:08b69c87240fadba66c946ff66b645ca69f32f49a7057d69509af1b6fce4f840", + "sourceBlob": "087430031d41af969abfb553a90fdbc49913e236", "status": "translated" }, "tutorials/new-as-model.md": { "sourceHash": "sha256:617f37b605f71ee0f770b896db1166d532fa0b21fb5e9c084cccfd12324bcbc9", + "sourceBlob": "78979a6f1f9cec1aabf2ba6c4ffeaad88b75935b", "status": "translated" }, "tutorials/new-pbi-model.md": { "sourceHash": "sha256:01b20064a80073530281396fa9208b87035a900d9ed7f608fb741cfc09302af4", + "sourceBlob": "807fddf3f679e02e6c641299b610e140a2ffb60a", "status": "translated" }, "tutorials/powerbi-xmla.md": { "sourceHash": "sha256:bcb5a43d97006c2e63735cda05f2888cecdee8ed344eb20bf4e723d72ab95cdf", + "sourceBlob": "dbcf16ba026efd18ef05a0cba40c8b8b1a2a18e7", "status": "translated" }, "tutorials/toc.md": { "sourceHash": "sha256:8a2515bffeac53dd28f78da28c6c321a4c7687437cb4952039b1f230347996b0", + "sourceBlob": "597c98218ac1b44f1503bdd95cc0c54d5da33595", "status": "translated" }, "tutorials/udfs.md": { "sourceHash": "sha256:4c17768a0dc089431a06fa79ed1785918e2bf24339d93c0976fcbc9781544566", + "sourceBlob": "1d54ec06bb59b37caef36ccb4cc67c82fc24089e", "status": "translated" }, "tutorials/user-defined-aggregations.md": { "sourceHash": "sha256:b5f8ded1057c581a96e8d3f46fea892ea20c77dc028d681125a70abb77290a22", + "sourceBlob": "7b6b8a270ab729cc696e138135f3eba22358001e", "status": "translated" }, "tutorials/workspace-mode.md": { "sourceHash": "sha256:e35e6f82ed64cb2d6a73fe987b64d631ebd2416fba3bb1055e56bb91c89d4eed", + "sourceBlob": "ec8fad8afd55f4c28bc7d290a82e2335fccb7068", "status": "translated" }, "whats-new/3-11-0.html": { @@ -1840,13 +2264,13 @@ } }, "summary": { - "translated": 448, + "translated": 452, "outdated": 0, - "untranslated": 10, + "untranslated": 6, "copied": 1, "pinned": 0, "total": 459, - "completionPercent": 97.8, + "completionPercent": 98.7, "failures": 0 } } diff --git a/localizedContent/zh/content/references/downloads.md b/localizedContent/zh/content/references/downloads.md index 7a0018357..907a26486 100644 --- a/localizedContent/zh/content/references/downloads.md +++ b/localizedContent/zh/content/references/downloads.md @@ -1,8 +1,8 @@ ---- -uid: 下载量 +--- +uid: downloads title: 全部下载 author: Daniel Otykier -updated: 2026-09-22 +updated: 2026-10-11 applies_to: products: - product: Tabular Editor 2 @@ -23,25 +23,25 @@ applies_to: ## 最新版本 -Tabular Editor 3.27.0.0(_[发布说明](release-notes/3_27_0_0.md)_)提供两个构建版本:一个基于 .NET 10,另一个基于 .NET 8。这两个构建版本在功能上完全一致,并且可以并行安装。 +Tabular Editor 3.27.1.0(_[发布说明](release-notes/3_27_1_0.md)_)提供两个构建版本:一个基于 .NET 10,另一个基于 .NET 8。这两个构建版本在功能上完全一致,并且可以并行安装。 ### 基于 .NET 10(推荐) 需要 [.NET Desktop Runtime 10](https://dotnet.microsoft.com/download/dotnet/10.0)。 `.exe` 安装程序会提示为你下载并安装该运行时;便携式 `.zip` 为自包含版本,完全不需要任何运行时。 -- 下载 [Tabular Editor 3.27.0.0(64 位)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe) _(推荐)_ -- 下载 [Tabular Editor 3.27.0.0(ARM64)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) -- 便携版本:[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) -- MSI 版:[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.msi),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.msi) +- 下载 [Tabular Editor 3.27.1.0(64 位)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe) _(推荐)_ +- 下载 [Tabular Editor 3.27.1.0(ARM64)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) +- 便携版本:[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) +- MSI 版:[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) ### 基于 .NET 8 适用于暂时无法运行 .NET 10 桌面运行时的计算机。需要 [.NET Desktop Runtime 8](https://dotnet.microsoft.com/download/dotnet/8.0)。 -- 下载 [Tabular Editor 3.27.0.0(64 位)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net8.exe) -- 下载 [Tabular Editor 3.27.0.0(ARM64)](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net8.exe) -- 便携版本:[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net8.zip),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net8.zip) -- MSI 版:[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net8.msi),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net8.msi) +- 下载 [Tabular Editor 3.27.1.0(64 位)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe) +- 下载 [Tabular Editor 3.27.1.0(ARM64)](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) +- 便携版本:[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) +- MSI 版:[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) > [!TIP] > 不确定该选哪个?请选择 .NET 10 x64 安装程序。仅在 ARM 架构的 PC 上才选择 ARM64;只有在你的组织无法安装 .NET 10 桌面运行时的情况下才选择 .NET 8。 @@ -58,6 +58,13 @@ Tabular Editor 3.27.0.0(_[发布说明](release-notes/3_27_0_0.md)_)提供 ## 更新历史 +- 2026-10-15 **Tabular Editor 3.27.1.0** (_[发布说明](release-notes/3_27_1_0.md)_) + - .NET 10 安装程序(.exe):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) + - .NET 10 便携版(.zip):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) + - .NET 10 安装程序(.msi):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) + - .NET 8 安装程序(.exe):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) + - .NET 8 便携版(.zip):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) + - .NET 8 安装程序(.msi):[x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi),[ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) - 2026-09-22 **Tabular Editor 3.27.0.0** (_[发布说明](release-notes/3_27_0_0.md)_) - .NET 10 安装程序(.exe):[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) - .NET 10 便携版(.zip):[x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip),[ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) diff --git a/localizedContent/zh/content/references/release-history.md b/localizedContent/zh/content/references/release-history.md index 84a63f380..bb3f16b1d 100644 --- a/localizedContent/zh/content/references/release-history.md +++ b/localizedContent/zh/content/references/release-history.md @@ -1,10 +1,18 @@ --- uid: release-history title: 完整发布历史 +updated: 2026-10-11 --- # 完整发布历史 +- 2026-10-15 **Tabular Editor 3.27.1.0** (_[发布说明](release-notes/3_27_1_0.md)_) + - .NET 10 安装程序(.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net10.exe) + - .NET 10 便携版(.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.zip) + - .NET 10 安装程序(.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net10.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net10.msi) + - .NET 8 安装程序(.exe): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.x64.Net8.exe), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.Installer.ARM64.Net8.exe) + - .NET 8 便携版(.zip): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.zip), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.zip) + - .NET 8 安装程序(.msi): [x64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.x64.Net8.msi), [ARM64](https://cdn.tabulareditor.com/files/archive/te3/3.27.1.0/TabularEditor.3.27.1.0.ARM64.Net8.msi) - 2026-09-22 **Tabular Editor 3.27.0.0** (_[发布说明](release-notes/3_27_0_0.md)_) - .NET 10 安装程序(.exe): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.x64.Net10.exe), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.Installer.ARM64.Net10.exe) - .NET 10 便携版(.zip): [x64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.x64.Net10.zip), [ARM64](https://cdn.tabulareditor.com/files/TabularEditor.3.27.0.0.ARM64.Net10.zip) diff --git a/localizedContent/zh/content/references/release-notes/3_0_3.md b/localizedContent/zh/content/references/release-notes/3_0_3.md index f70303071..fa6409aac 100644 --- a/localizedContent/zh/content/references/release-notes/3_0_3.md +++ b/localizedContent/zh/content/references/release-notes/3_0_3.md @@ -36,7 +36,7 @@ _如果你此前未使用过 Tabular Editor 3,则有资格申请 30 天试用 ## 从 Tabular Editor 2.x 升级而来? -观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 另外,也别忘了查看我们的 [入门指南](https://docs.tabulareditor.com/onboarding/index.html)。 +观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 **Tabular Editor 3 主要功能概览:** diff --git a/localizedContent/zh/content/references/release-notes/3_1_2.md b/localizedContent/zh/content/references/release-notes/3_1_2.md index 666ca57d9..ad622ca95 100644 --- a/localizedContent/zh/content/references/release-notes/3_1_2.md +++ b/localizedContent/zh/content/references/release-notes/3_1_2.md @@ -52,7 +52,7 @@ _如果你此前未使用过 Tabular Editor 3,则有资格申请 30 天试用 ## 从 Tabular Editor 2.x 升级而来? -观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 另外,也别忘了查看我们的 [入门指南](https://docs.tabulareditor.com/onboarding/index.html)。 +观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 **Tabular Editor 3 主要功能概览:** diff --git a/localizedContent/zh/content/references/release-notes/3_3_4.md b/localizedContent/zh/content/references/release-notes/3_3_4.md index 7ae5b0d74..17b3b556e 100644 --- a/localizedContent/zh/content/references/release-notes/3_3_4.md +++ b/localizedContent/zh/content/references/release-notes/3_3_4.md @@ -55,7 +55,7 @@ _如果你此前未使用过 Tabular Editor 3,则有资格申请 30 天试用 ## 从 Tabular Editor 2.x 升级而来? -观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 另外,也别忘了查看我们的 [入门指南](https://docs.tabulareditor.com/onboarding/index.html)。另外,请务必查看我们的[上手指南](https://docs.tabulareditor.com/onboarding/index.html)。 +观看 [此视频](https://www.youtube.com/watch?v=pt3DdcjfImY),快速了解 Tabular Editor 3 的新功能。 另外,也别忘了查看我们的 [入门指南](https://docs.tabulareditor.com/onboarding/index.html)。 **Tabular Editor 3 主要功能概览:** diff --git a/localizedContent/zh/content/references/user-options.md b/localizedContent/zh/content/references/user-options.md index fb1f4eef0..ce4db55d4 100644 --- a/localizedContent/zh/content/references/user-options.md +++ b/localizedContent/zh/content/references/user-options.md @@ -1,8 +1,8 @@ ---- +--- uid: user-options title: 用户选项(.tmuo)文件 author: Daniel Otykier -updated: 2026-09-15 +updated: 2026-10-12 applies_to: products: - product: Tabular Editor 2 @@ -25,9 +25,9 @@ Tabular Editor 3 引入了一种新的基于 JSON 的文件,用于存储开发 | 您打开的项目 | 用户选项文件 | | ----------------------------------------------------------------------- | -------------------------------------- | -| `SpaceParts.bim`,或任何 `.bim` / `.pbit` / `Database.json` 文件 | `SpaceParts..tmuo`,与其位于同一目录中 | +| `SpaceParts.bim`,或任何 `.bim` / `.pbit` / `database.json` 文件 | `SpaceParts..tmuo`,与其位于同一目录中 | | `model.tmdl` 或 `database.tmdl` 文件 | `model..tmuo`,与其位于同一目录中 | -| 包含 `Database.json` 的文件夹 | `database..tmuo`,位于该文件夹中 | +| 包含 `database.json` 的文件夹 | `database..tmuo`,位于该文件夹中 | | 包含 Tabular Model Definition Language (TMDL) 模型文件的文件夹 | `model..tmuo`,位于该文件夹中 | TMDL 始终使用 `model` 前缀,以延续早于 `database.tmdl` 的版本中的命名方式。 @@ -106,10 +106,10 @@ TMDL 始终使用 `model` 前缀,以延续早于 `database.tmdl` 的版本中 | `Password` | 加密对象 | 已设置,_并且_你已选择将其保存。 | | `Provider` | 字符串 | `ServerType` 为 `OleDb`。 | | `PasswordKey`、`UserIdKey` | 字符串 | `ServerType` 为 `OleDb`,且已设置对应的值。 | -| `服务器` | 字符串 | `ServerType` 不是 `Odbc`,且已设置。 | +| `Server` | 字符串 | `ServerType` 不是 `Odbc`,且已设置。 | | `ServerKey`、`DatabaseKey` | 字符串 | `ServerType` 不是 `Sql`,且已设置该值。 | -| `数据库` | 字符串 | 已设置。 | -| `身份验证` | 字符串 | `ServerType` 可以是 `Sql`、`OleDb` 或 `Databricks`。 `Sql`、`WindowsIntegrated`、`AadInteractive`、`AadPassword`、`AadIntegrated`、`AadServicePrincipal`、`AccessToken` 或 `OAuth` 之一。 | +| `Database` | 字符串 | 已设置。 | +| `Authentication` | 字符串 | `ServerType` 可以是 `Sql`、`OleDb` 或 `Databricks`。 `Sql`、`WindowsIntegrated`、`AadInteractive`、`AadPassword`、`AadIntegrated`、`AadServicePrincipal`、`AccessToken` 或 `OAuth` 之一。 | | `Encrypt` | 布尔值 | 无论设为 true 还是 false,都会写入。 | | `Dsn` | 字符串 | 已设置时写入。用于 ODBC。 | | `RowLimitClause`、`IdentifierQuoting` | 数字 | 不为默认值时写入。 | @@ -121,7 +121,7 @@ TMDL 始终使用 `model` 前缀,以延续早于 `database.tmdl` 的版本中 ### 部署 -`Deployment` 保存 Deployment Wizard 上次用于此模型的内容。一旦该块存在,这六个布尔选项就会始终写入,无论其值为 true 还是 false: +`Deployment` 保存 Deployment Wizard 上次用于此模型的内容。一旦该块存在,这七个布尔选项就会始终写入,无论其值为 true 还是 false: | 属性 | 类型 | 写入时机 | | ------------------------------- | ------ | ------------------------------------- | @@ -131,6 +131,7 @@ TMDL 始终使用 `model` 前缀,以延续早于 `database.tmdl` 的版本中 | `DeployModelRoles` | 布尔值 | 始终 | | `DeployModelRoleMembers` | 布尔值 | 始终 | | `DeploySharedExpressions` | 布尔值 | 始终 | +| `DeployLinguisticMetadata` | 布尔值 | 始终 | | `TargetConnectionString` | 字符串或对象 | 始终返回;未设置时为 `null`。当连接字符串包含密码时,使用对象形式。 | | `TargetDatabase` | 字符串 | 始终返回;未设置时为 `null`。 | | `TargetCredentials` | 对象 | 用户名和密码均已设置。 | diff --git a/localizedContent/zh/content/security/security-privacy.md b/localizedContent/zh/content/security/security-privacy.md index 90e687d1d..b7c21f2fd 100644 --- a/localizedContent/zh/content/security/security-privacy.md +++ b/localizedContent/zh/content/security/security-privacy.md @@ -23,7 +23,7 @@ applies_to: ## Microsoft 关于 Tabular Editor 等第三方工具的建议 -Microsoft 在此处说明其支持使用社区第三方工具:[用于开发企业级 Power BI 和 Analysis Services 模型的社区和第三方工具](https://powerbi.microsoft.com/en-us/blog/community-tools-for-enterprise-powerbi-and-analysisservices) +Microsoft 在此处说明其支持使用社区第三方工具:[用于开发企业级 Power BI 和 Analysis Services 模型的社区和第三方工具]( https://powerbi.microsoft.com/en-us/blog/community-tools-for-enterprise-powerbi-and-analysisservices) Microsoft的 Power BI 实施规划文档在高级数据建模场景与企业级开发中明确提及 Tabular Editor:[Power BI 使用场景:高级 Data model 管理](https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-usage-scenario-advanced-data-model-management#tabular-editor) diff --git a/metadata/build-config.json b/metadata/build-config.json index d5142b785..c1f92a003 100644 --- a/metadata/build-config.json +++ b/metadata/build-config.json @@ -32,6 +32,12 @@ { "pattern": "security/third-party-notices.md" } ], "ignore": [], + "segmentDiff": { + "_comment": "Outdated translations are updated from changed segments only (Markdown blocks, JSON keys, toc names). A file whose changed text exceeds maxChangedRatio of its translatable text is sent whole. contextSegments: unchanged English blocks sent on each side of a change as untranslatable context (-1 = the whole page, 0 = none); Translated does not bill context. The repository variable TRANSLATED_CONTEXT_SEGMENTS overrides it.", + "enabled": true, + "maxChangedRatio": 0.8, + "contextSegments": 2 + }, "sandboxLimits": { "_comment": "Translated warns that excessive sandbox use can get the sandbox key blocked. In sandbox mode a run refuses to exceed these unless --allow-unbounded is given.", "maxFilesPerRun": 20, diff --git a/pyproject.toml b/pyproject.toml index 3b6258e53..8dbea86b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ include = [ "build_scripts/csharp_doctest.py", "build_scripts/te_script_runner.py", "build_scripts/translate-content.py", + "build_scripts/translation_segments.py", ] [tool.mypy] @@ -23,5 +24,6 @@ files = [ "build_scripts/csharp_doctest.py", "build_scripts/te_script_runner.py", "build_scripts/translate-content.py", + "build_scripts/translation_segments.py", ] strict = true