Skip to content

Commit e100c31

Browse files
authored
Initial Translations for Chinese and Spanis
1 parent ac953dc commit e100c31

747 files changed

Lines changed: 42164 additions & 39065 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ swa start _site
5050
| `--serve` | Build and serve locally (English only, for development) |
5151
| `--skip-gen` | Skip running gen_redirects.py (use existing configs) |
5252
| `--no-api-copy` | Skip copying API docs to localized sites |
53+
| `--sync` | Sync English fallback for missing/outdated translations (for local dev) |
5354

5455
## What the Build Script Does
5556

5657
1. **Generates DocFX configurations** - Runs `gen_redirects.py` to create `docfx.json` for each language
5758
2. **Generates language manifest** - Creates `metadata/languages.json` for runtime language switching
58-
3. **Syncs content** - Copies English source content; uses English as fallback for missing translations. Readds the english file if the file is modified in content or deleted in translation.
59+
3. **Syncs content** - Copies English source to `localizedContent/en/`. For other languages, only shared directories (assets, api) are synced by default since Crowdin manages translations. Use `--sync` to enable full English fallback for missing/outdated translations (useful for local development).
5960
4. **Builds documentation** - Runs DocFX for each requested language
6061
5. **Fixes API docs** - Patches xref links in generated API documentation
6162
6. **Copies API docs** - Shares English API docs with localized sites
@@ -101,7 +102,20 @@ TEDoc/
101102
4. Add a translated `_ui-strings.json` to the content subdirectory (see [Translating UI Strings](#translating-ui-strings) below). If no translation is provided, an automatic fallback will be generated.
102103
5. Run `python build-docs.py --all` to generate configs and build. Language will be added dynamically to language picker.
103104

104-
> **Note:** English content from `content/` is automatically copied to `localizedContent/en/content/` during build. For other languages, English content is used as fallback for missing translations. This includes `_ui-strings.json` — if no translated version exists, English UI strings are used.
105+
> **Note:** English content from `content/` is automatically copied to `localizedContent/en/content/` during build. For other languages, Crowdin manages translations via PRs. Shared directories (assets, api) are always synced from English. To use English as fallback for missing/outdated translations during local development, add the `--sync` flag.
106+
107+
# Bookmark Links and Translations
108+
109+
When linking to a specific heading within a page (e.g., `#my-heading`), the anchor ID is auto-generated from the heading text. When headings are translated by Crowdin, the anchor changes, breaking bookmark links.
110+
111+
To prevent this, add an `<a name="..."></a>` tag above any heading that is referenced by a bookmark link:
112+
113+
```markdown
114+
<a name="my-heading"></a>
115+
## My Heading
116+
```
117+
118+
Crowdin does not translate HTML `name` attributes, so the anchor remains stable across all languages. Only add these to headings that are actually linked to — there is no need to add them to every heading.
105119

106120
# Translating UI Strings
107121

@@ -137,4 +151,18 @@ If a key is missing from a language's file, or no `_ui-strings.json` exists at a
137151
| `footer.privacyPolicy` | `Privacy & Cookie policy` | Footer bottom link |
138152
| `footer.termsConditions` | `Terms & Conditions` | Footer bottom link |
139153
| `footer.licenseTerms` | `License terms` | Footer bottom link |
140-
154+
| `appliesTo` | `Applies to: ` | "Applies to" label on article metadata |
155+
| `availableSince` | `Available since` | Version availability label (e.g., "Available since 3.5.0") |
156+
| `availableIn` | `Available in` | Version range label (e.g., "Available in 3.5.0–3.8.0") |
157+
| `inThisArticle` | `In this article` | Sidebar table of contents heading |
158+
| `searchResultsCount` | `{count} results for "{query}"` | Search results summary |
159+
| `searchNoResults` | `No results for "{query}"` | No search results message |
160+
| `tocFilter` | `Filter by title` | TOC filter input placeholder |
161+
| `nextArticle` | `Next` | Next article navigation |
162+
| `prevArticle` | `Previous` | Previous article navigation |
163+
| `themeLight` | `Light` | Theme picker option |
164+
| `themeDark` | `Dark` | Theme picker option |
165+
| `themeAuto` | `Auto` | Theme picker option |
166+
| `changeTheme` | `Change theme` | Theme picker label |
167+
| `copy` | `Copy` | Code block copy button |
168+
| `downloadPdf` | `Download PDF` | PDF download button |

build-docs.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,35 +69,44 @@ def get_available_languages() -> list[str]:
6969
])
7070

7171

72-
def prepare_localized_content(lang: str) -> int:
72+
def prepare_localized_content(lang: str, sync: bool = False) -> int:
7373
"""Run sync-localized-content.py for a language.
74-
75-
For English: copies all source content
76-
For other languages: syncs with translation tracking,
77-
falls back to English for outdated/missing files
74+
75+
For English: always copies all source content (required for docfx)
76+
For other languages:
77+
sync=True: full English fallback sync (hash comparison, copy missing/outdated)
78+
sync=False: only sync shared directories (assets, api) — Crowdin manages translations
7879
"""
7980
if lang == "en":
80-
description = "Syncing English content from source"
81+
# English always needs full sync
82+
return run_command(
83+
[sys.executable, "build_scripts/sync-localized-content.py", "--sync", "en"],
84+
"Syncing English content from source"
85+
)
86+
87+
if sync:
88+
return run_command(
89+
[sys.executable, "build_scripts/sync-localized-content.py", "--sync", lang],
90+
f"Syncing {lang} content (fallback to English for outdated)"
91+
)
8192
else:
82-
description = f"Syncing {lang} content (fallback to English for outdated)"
83-
84-
return run_command(
85-
[sys.executable, "build_scripts/sync-localized-content.py", "--sync", lang],
86-
description
87-
)
93+
return run_command(
94+
[sys.executable, "build_scripts/sync-localized-content.py", "--shared-only", lang],
95+
f"Syncing shared directories for {lang}"
96+
)
8897

8998

90-
def build_language(lang: str) -> int:
99+
def build_language(lang: str, sync: bool = False) -> int:
91100
"""Build documentation for a specific language."""
92101
config_path = f"localizedContent/{lang}/docfx.json"
93-
102+
94103
if not os.path.exists(config_path):
95104
print(f"Error: Config file not found: {config_path}")
96105
print("Run 'python gen_redirects.py' first to generate configs.")
97106
return 1
98-
107+
99108
# Prepare content (copy from source for en, or fallbacks for other langs)
100-
result = prepare_localized_content(lang)
109+
result = prepare_localized_content(lang, sync=sync)
101110
if result != 0:
102111
return result
103112

@@ -213,6 +222,7 @@ def main() -> int:
213222
parser.add_argument("--serve", action="store_true", help="Build English and serve locally")
214223
parser.add_argument("--skip-gen", action="store_true", help="Skip gen_redirects.py")
215224
parser.add_argument("--no-api-copy", action="store_true", help="Skip copying API docs")
225+
parser.add_argument("--sync", action="store_true", help="Sync English fallback for missing/outdated translations (for local dev)")
216226

217227
args = parser.parse_args()
218228

@@ -247,7 +257,7 @@ def main() -> int:
247257

248258
if args.serve:
249259
# Build English only and serve
250-
result = build_language("en")
260+
result = build_language("en", sync=True)
251261
if result != 0:
252262
return result
253263

@@ -286,7 +296,7 @@ def main() -> int:
286296

287297
# Build all requested languages
288298
for lang in build_langs:
289-
result = build_language(lang)
299+
result = build_language(lang, sync=args.sync)
290300
if result != 0:
291301
return result
292302

build_scripts/sync-localized-content.py

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
python sync-localized-content.py --status # Show all languages
1616
python sync-localized-content.py --status es # Show Spanish details
1717
python sync-localized-content.py --sync es # Sync Spanish content
18+
python sync-localized-content.py --shared-only es # Sync only shared dirs (assets, api)
1819
python sync-localized-content.py --init es # Initialize tracking
1920
python sync-localized-content.py --mark-translated es # Mark all as translated
2021
python sync-localized-content.py --json # JSON output for CI
@@ -25,7 +26,6 @@
2526
import os
2627
import shutil
2728
import sys
28-
from datetime import datetime, timezone
2929
from pathlib import Path
3030
from typing import Any
3131

@@ -56,7 +56,6 @@ def load_translation_status(lang: str) -> dict[str, Any]:
5656
if not status_path.exists():
5757
return {
5858
"language": lang,
59-
"lastSync": None,
6059
"sourceBaseline": str(CONTENT_DIR),
6160
"files": {},
6261
"summary": {
@@ -77,9 +76,6 @@ def save_translation_status(lang: str, status: dict[str, Any]) -> None:
7776
status_path = LOCALIZED_DIR / lang / STATUS_FILENAME
7877
status_path.parent.mkdir(parents=True, exist_ok=True)
7978

80-
# Update timestamp
81-
status["lastSync"] = datetime.now(timezone.utc).isoformat()
82-
8379
# Recalculate summary
8480
files = status.get("files", {})
8581
translated = sum(1 for f in files.values() if f.get("status") == STATUS_TRANSLATED)
@@ -156,27 +152,21 @@ def check_translation_status(lang: str, source_files: dict[str, str]) -> dict[st
156152
new_files[rel_path] = {
157153
"sourceHash": source_hash,
158154
"status": STATUS_UNTRANSLATED,
159-
"lastChecked": datetime.now(timezone.utc).isoformat()
160155
}
161156
else:
162157
stored_hash = file_info.get("sourceHash", "")
163-
158+
164159
if stored_hash == source_hash:
165160
# Source hasn't changed, translation is current
166161
new_files[rel_path] = {
167162
"sourceHash": source_hash,
168163
"status": STATUS_TRANSLATED,
169-
"lastChecked": datetime.now(timezone.utc).isoformat(),
170-
"translatedAt": file_info.get("translatedAt", datetime.now(timezone.utc).isoformat())
171164
}
172165
else:
173166
# Source changed since translation was made
174167
new_files[rel_path] = {
175168
"sourceHash": source_hash,
176169
"status": STATUS_OUTDATED,
177-
"previousHash": stored_hash,
178-
"lastChecked": datetime.now(timezone.utc).isoformat(),
179-
"translatedAt": file_info.get("translatedAt")
180170
}
181171

182172
status["files"] = new_files
@@ -208,7 +198,6 @@ def sync_language(lang: str, source_files: dict[str, str], dry_run: bool = False
208198
shutil.copy2(source_file, dest_file)
209199
# Update status to untranslated (since we replaced with English)
210200
file_info["status"] = STATUS_UNTRANSLATED
211-
file_info["replacedAt"] = datetime.now(timezone.utc).isoformat()
212201
counts["replaced"] += 1
213202
print(f" Replaced (outdated): {rel_path}")
214203
elif file_status == STATUS_UNTRANSLATED:
@@ -289,6 +278,31 @@ def sync_english(dry_run: bool = False) -> dict[str, int]:
289278
return counts
290279

291280

281+
def sync_shared_only(lang: str, dry_run: bool = False) -> dict[str, int]:
282+
"""Sync only shared directories (assets, api) for a language.
283+
284+
Used when full translation sync is disabled (Crowdin manages translations).
285+
Skips hash comparison and translation status tracking.
286+
"""
287+
localized_content_dir = LOCALIZED_DIR / lang / "content"
288+
counts = {"synced_dirs": 0}
289+
290+
if not dry_run:
291+
localized_content_dir.mkdir(parents=True, exist_ok=True)
292+
293+
for dir_name in get_shared_directories():
294+
src = CONTENT_DIR / dir_name
295+
dest = localized_content_dir / dir_name
296+
if src.exists() and not dry_run:
297+
if dest.exists():
298+
shutil.rmtree(dest)
299+
shutil.copytree(src, dest)
300+
print(f" Synced shared: {dir_name}/")
301+
counts["synced_dirs"] += 1
302+
303+
return counts
304+
305+
292306
def init_language(lang: str, source_files: dict[str, str]) -> None:
293307
"""Initialize translation tracking for an existing language.
294308
@@ -302,7 +316,6 @@ def init_language(lang: str, source_files: dict[str, str]) -> None:
302316

303317
status = {
304318
"language": lang,
305-
"lastSync": datetime.now(timezone.utc).isoformat(),
306319
"sourceBaseline": str(CONTENT_DIR),
307320
"files": {}
308321
}
@@ -315,15 +328,12 @@ def init_language(lang: str, source_files: dict[str, str]) -> None:
315328
status["files"][rel_path] = {
316329
"sourceHash": source_hash,
317330
"status": STATUS_TRANSLATED,
318-
"lastChecked": datetime.now(timezone.utc).isoformat(),
319-
"translatedAt": datetime.now(timezone.utc).isoformat()
320331
}
321332
else:
322333
# File missing - mark as untranslated
323334
status["files"][rel_path] = {
324335
"sourceHash": source_hash,
325336
"status": STATUS_UNTRANSLATED,
326-
"lastChecked": datetime.now(timezone.utc).isoformat()
327337
}
328338

329339
save_translation_status(lang, status)
@@ -340,20 +350,17 @@ def mark_translated(lang: str, file_paths: list[str] | None, source_files: dict[
340350
If file_paths is None, marks all files in the language.
341351
"""
342352
status = load_translation_status(lang)
343-
now = datetime.now(timezone.utc).isoformat()
344-
353+
345354
if file_paths is None:
346355
# Mark all files
347356
file_paths = list(source_files.keys())
348-
357+
349358
updated = 0
350359
for rel_path in file_paths:
351360
if rel_path in source_files:
352361
status["files"][rel_path] = {
353362
"sourceHash": source_files[rel_path],
354363
"status": STATUS_TRANSLATED,
355-
"lastChecked": now,
356-
"translatedAt": now
357364
}
358365
updated += 1
359366

@@ -457,6 +464,11 @@ def main() -> int:
457464
metavar="LANG",
458465
help="Sync content for a language (use 'en' for English)"
459466
)
467+
parser.add_argument(
468+
"--shared-only",
469+
metavar="LANG",
470+
help="Sync only shared directories (assets, api) for a language"
471+
)
460472
parser.add_argument(
461473
"--init",
462474
metavar="LANG",
@@ -484,10 +496,18 @@ def main() -> int:
484496
)
485497

486498
args = parser.parse_args()
487-
499+
500+
# --shared-only doesn't need source file hashes, handle it early
501+
if args.shared_only:
502+
lang = args.shared_only
503+
print(f"Syncing shared directories for '{lang}'...")
504+
counts = sync_shared_only(lang, args.dry_run)
505+
print(f"\nShared sync complete: {counts['synced_dirs']} directory(ies) synced")
506+
return 0
507+
488508
# Get source files (needed for most operations)
489509
source_files = get_source_files()
490-
510+
491511
if args.status:
492512
if args.status == "__all__":
493513
print_status_summary(args.json)
@@ -498,7 +518,7 @@ def main() -> int:
498518
if args.sync:
499519
lang = args.sync
500520
print(f"Syncing content for '{lang}'...")
501-
521+
502522
if lang == "en":
503523
counts = sync_english(args.dry_run)
504524
print(f"\nEnglish sync complete: {counts['copied']} files copied")
@@ -509,7 +529,7 @@ def main() -> int:
509529
print(f" Replaced (outdated->English): {counts['replaced']}")
510530
print(f" Copied (new->English): {counts['copied']}")
511531
return 0
512-
532+
513533
if args.init:
514534
init_language(args.init, source_files)
515535
return 0

content/_ui-strings.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,20 @@
2020
"footer.technicalSupport": "Technical Support",
2121
"footer.privacyPolicy": "Privacy & Cookie policy",
2222
"footer.termsConditions": "Terms & Conditions",
23-
"footer.licenseTerms": "License terms"
23+
"footer.licenseTerms": "License terms",
24+
"appliesTo": "Applies to: ",
25+
"availableSince": "Available since",
26+
"availableIn": "Available in",
27+
"inThisArticle": "In this article",
28+
"searchResultsCount": "{count} results for \"{query}\"",
29+
"searchNoResults": "No results for \"{query}\"",
30+
"tocFilter": "Filter by title",
31+
"nextArticle": "Next",
32+
"prevArticle": "Previous",
33+
"themeLight": "Light",
34+
"themeDark": "Dark",
35+
"themeAuto": "Auto",
36+
"changeTheme": "Change theme",
37+
"copy": "Copy",
38+
"downloadPdf": "Download PDF"
2439
}

content/features/code-actions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ In the screenshot below, for example, the **Prefix variable with '_'** action ca
7474

7575
![Code Action All Occurrences](~/content/assets/images/features/code-action-all-occurrences.png)
7676

77+
<a name="list-of-code-actions"></a>
7778
## List of Code Actions
7879

7980
The table below lists all currently available Code Actions. You can toggle off Code Actions in the **Tools > Preferences** dialog under **Text Editors > DAX Editor > Code Actions** (a future update will let you toggle individual actions for a more customized experience). Some Code Actions also have additional configuration options, such as which prefix to use for variable names.

content/features/csharp-scripts.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ In addition, the following .NET Framework assemblies are loaded by default:
282282
- TabularEditor.Exe
283283
- Microsoft.AnalysisServices.Tabular.Dll
284284

285+
<a name="accessing-environment-variables"></a>
285286
## Accessing Environment Variables
286287

287288
When running C# scripts via the Tabular Editor CLI (especially in CI/CD pipelines), you can pass parameters to your scripts using environment variables. This is the recommended approach, as C# scripts executed by Tabular Editor CLI don't support traditional command-line arguments.
@@ -362,6 +363,7 @@ foreach(var table in Model.Tables)
362363
Info($"Configured model for {environment} environment");
363364
```
364365

366+
<a name="compatibility"></a>
365367
## Compatibility
366368

367369
The scripting APIs for Tabular Editor 2 and Tabular Editor 3 are mostly compatible, however, there are cases where you want to conditionally compile code depending on which version you're using. For this, you can use preprocessor directives, which were introduced in Tabular Editor 3.10.0.

0 commit comments

Comments
 (0)