-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathbuild-docs.py
More file actions
388 lines (308 loc) · 12.5 KB
/
build-docs.py
File metadata and controls
388 lines (308 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Unified documentation build script for multi-language support.
Usage:
python build-docs.py # Build all languages (default)
python build-docs.py --all # Build all languages
python build-docs.py --lang en # Build English only
python build-docs.py --lang es zh # Build specific languages
python build-docs.py --list # List available languages
python build-docs.py --serve # Build English and serve locally
Options:
--all Build all available languages
--lang LANGS Build specific language(s) (space-separated)
--list List available languages and exit
--serve Build and serve locally (English only, for development)
--skip-gen Skip running gen_redirects.py (use existing configs)
--no-api-copy Skip copying API docs to localized sites
"""
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
_DOCFX_WARNING_RE = re.compile(r': warning ', re.IGNORECASE)
def run_command(cmd: list[str], description: str, check: bool = True, fail_on_warnings: bool = False) -> int:
"""Run a command and return exit code.
If fail_on_warnings=True, streams output line-by-line, counts DocFX warning
diagnostics (lines matching ': warning '), and returns exit code 1 if any
are found — even when the process itself exits 0.
"""
print(f"\n{'='*60}")
print(f" {description}")
print(f"{'='*60}")
print(f"Running: {' '.join(cmd)}\n")
if fail_on_warnings:
warning_count = 0
process = subprocess.Popen(
cmd,
shell=(os.name == 'nt'),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
errors='replace'
)
for line in process.stdout:
print(line, end='', flush=True)
if _DOCFX_WARNING_RE.search(line):
warning_count += 1
process.wait()
if check and process.returncode != 0:
print(f"Error: Command failed with exit code {process.returncode}")
return process.returncode
if warning_count > 0:
print(f"\nError: DocFX produced {warning_count} warning(s). Failing build.")
return 1
return process.returncode
result = subprocess.run(cmd, shell=(os.name == 'nt'))
if check and result.returncode != 0:
print(f"Error: Command failed with exit code {result.returncode}")
return result.returncode
return result.returncode
def get_available_languages() -> list[str]:
"""Get list of available languages from metadata/languages.json or scan localizedContent/."""
manifest_path = Path("metadata/languages.json")
if manifest_path.exists():
with open(manifest_path) as f:
data = json.load(f)
# Handle both simple array and rich metadata formats
languages = data.get("languages", [])
if languages and isinstance(languages[0], dict):
return [lang["code"] for lang in languages]
return languages
# Fallback: scan localizedContent/ directly
localized_dir = Path("localizedContent")
if not localized_dir.exists():
return []
return sorted([
d.name for d in localized_dir.iterdir()
if d.is_dir() and len(d.name) <= 5
])
def prepare_localized_content(lang: str, sync: bool = False) -> int:
"""Run sync-localized-content.py for a language.
For English: always copies all source content (required for docfx)
For other languages:
sync=True: full English fallback sync (hash comparison, copy missing/outdated)
sync=False: only sync shared directories (assets, api) — Crowdin manages translations
"""
if lang == "en":
# English always needs full sync
return run_command(
[sys.executable, "build_scripts/sync-localized-content.py", "--sync", "en"],
"Syncing English content from source"
)
if sync:
return run_command(
[sys.executable, "build_scripts/sync-localized-content.py", "--sync", lang],
f"Syncing {lang} content (fallback to English for outdated)"
)
else:
return run_command(
[sys.executable, "build_scripts/sync-localized-content.py", "--shared-only", lang],
f"Syncing shared directories for {lang}"
)
def build_language(lang: str, sync: bool = False) -> int:
"""Build documentation for a specific language."""
config_path = f"localizedContent/{lang}/docfx.json"
if not os.path.exists(config_path):
print(f"Error: Config file not found: {config_path}")
print("Run 'python gen_redirects.py' first to generate configs.")
return 1
# Prepare content (copy from source for en, or fallbacks for other langs)
result = prepare_localized_content(lang, sync=sync)
if result != 0:
return result
# Build the documentation — fail if DocFX emits any warnings
return run_command(
["docfx", config_path],
f"Building {lang} documentation",
fail_on_warnings=True
)
def copy_languages_manifest() -> int:
"""Copy languages.json to _site/ root for runtime access."""
manifest_src = Path("metadata/languages.json")
manifest_dest = Path("_site/languages.json")
if not manifest_src.exists():
print("Warning: languages.json not found, skipping copy")
return 0
# Ensure _site directory exists
manifest_dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(manifest_src, manifest_dest)
print(f"Copied languages.json to _site/")
return 0
def copy_404_to_root() -> int:
"""Copy 404.html from English site to _site/ root for SWA fallback."""
src_404 = Path("_site/en/404.html")
dest_404 = Path("_site/404.html")
if not src_404.exists():
print("Warning: _site/en/404.html not found, skipping 404 copy")
return 0
shutil.copy(src_404, dest_404)
print(f"Copied 404.html to _site/ root")
return 0
def copy_index_to_root() -> int:
"""Copy index.html redirect from content to _site/ root for SWA validation."""
src_index = Path("content/index.html")
dest_index = Path("_site/index.html")
if not src_index.exists():
print("Warning: content/index.html not found, skipping index copy")
return 0
shutil.copy(src_index, dest_index)
print(f"Copied index.html to _site/ root")
return 0
def copy_api_docs(languages: list[str]) -> int:
"""Copy API docs from English to localized sites."""
en_api = Path("_site/en/api")
if not en_api.exists():
print("Warning: English API docs not found, skipping API copy")
return 0
print(f"\n{'='*60}")
print(" Copying API docs to localized sites")
print(f"{'='*60}")
for lang in languages:
dest = Path(f"_site/{lang}/api")
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(en_api, dest)
print(f" Copied API docs to _site/{lang}/api")
return 0
def fix_xref_in_api() -> int:
"""Fix shared xref links in API HTML files."""
api_dir = Path("_site/en/api")
if not api_dir.exists():
return 0
print(f"\n{'='*60}")
print(" Fixing xref links in API docs")
print(f"{'='*60}")
count = 0
for html_file in api_dir.rglob("*.html"):
content = html_file.read_text(encoding="utf-8")
if '<a class="xref" href="TabularEditor.Shared.html">Shared</a>' in content:
content = content.replace(
'<a class="xref" href="TabularEditor.Shared.html">Shared</a>',
'Shared'
)
html_file.write_text(content, encoding="utf-8")
count += 1
print(f" Fixed {count} file(s)")
return 0
def main() -> int:
parser = argparse.ArgumentParser(
description="Build documentation for one or more languages",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument("--all", action="store_true", help="Build all languages")
parser.add_argument("--lang", nargs="+", help="Build specific language(s)")
parser.add_argument("--list", action="store_true", help="List available languages")
parser.add_argument("--serve", action="store_true", help="Build English and serve locally")
parser.add_argument("--skip-gen", action="store_true", help="Skip gen_redirects.py")
parser.add_argument("--no-api-copy", action="store_true", help="Skip copying API docs")
parser.add_argument("--sync", action="store_true", help="Sync English fallback for missing/outdated translations (for local dev)")
args = parser.parse_args()
# List available languages
if args.list:
langs = get_available_languages()
print("Available languages:")
for lang in langs:
suffix = " (default)" if lang == "en" else ""
print(f" {lang}{suffix}")
return 0
# Run gen_redirects.py first (unless skipped)
if not args.skip_gen:
result = run_command(
[sys.executable, "build_scripts/gen_redirects.py"],
"Generating docfx configurations"
)
if result != 0:
return result
# Generate languages manifest
result = run_command(
[sys.executable, "build_scripts/gen_languages.py"],
"Generating languages manifest"
)
if result != 0:
return result
# Determine which languages to build
available_langs = get_available_languages()
if args.serve:
# Build English only and serve
result = build_language("en", sync=True)
if result != 0:
return result
fix_xref_in_api()
copy_languages_manifest()
# Also copy to _site/en/ for local serving (docfx serve serves from en/)
manifest_src = Path("metadata/languages.json")
manifest_dest = Path("_site/en/languages.json")
if manifest_src.exists():
shutil.copy(manifest_src, manifest_dest)
print("Copied languages.json to _site/en/")
return run_command(
["docfx", "serve", "_site/en"],
"Serving documentation locally"
)
if args.lang:
# Build specific languages
build_langs = args.lang
# Validate languages
for lang in build_langs:
if lang not in available_langs:
print(f"Error: Language '{lang}' not found")
print(f"Available: {', '.join(available_langs)}")
return 1
else:
# Build all languages (default behavior)
build_langs = available_langs
# Ensure English is built first (needed for API docs)
if "en" in build_langs:
build_langs = ["en"] + [l for l in build_langs if l != "en"]
# Build all requested languages
for lang in build_langs:
result = build_language(lang, sync=args.sync)
if result != 0:
return result
if lang == "en":
fix_xref_in_api()
# Copy API docs to localized sites (non-English)
non_en_langs = [l for l in build_langs if l != "en"]
if non_en_langs and not args.no_api_copy and "en" in build_langs:
copy_api_docs(non_en_langs)
# Copy languages manifest to _site root
copy_languages_manifest()
# Copy 404.html to site root for SWA fallback
copy_404_to_root()
# Copy index.html to site root for SWA validation
copy_index_to_root()
# Inject SEO tags (hreflang, canonical) into HTML files for built languages
for lang in build_langs:
run_command(
[sys.executable, "build_scripts/inject_seo_tags.py", "--lang", lang],
f"Injecting SEO tags for {lang}"
)
# Generate staticwebapp.config.json for Azure SWA routing
run_command(
[sys.executable, "build_scripts/gen_staticwebapp_config.py"],
"Generating staticwebapp.config.json"
)
print(f"\n{'='*60}")
print(" Build complete!")
print(f"{'='*60}")
print(f"Output: _site/")
for lang in build_langs:
print(f" - {lang}/")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
print("\nBuild interrupted.")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)