-
Notifications
You must be signed in to change notification settings - Fork 67.6k
Expand file tree
/
Copy pathcorrect-translation-content.ts
More file actions
2237 lines (2114 loc) · 130 KB
/
Copy pathcorrect-translation-content.ts
File metadata and controls
2237 lines (2114 loc) · 130 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
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* A lot of translations have minor corruptions that will lead to rendering
* failing (and having to rely on English fallback). Many of these are
* easy to manually correct for.
*
* This function is a temporary solution to correct for these corruptions.
* It looks for easy "low hanging fruit" that we can correct for.
*
*/
interface CorrectionContext {
code?: string
dottedPath?: string
relativePath?: string
[key: string]: any
}
export function correctTranslatedContentStrings(
content: string,
englishContent: string,
context: CorrectionContext = {},
): string {
// --- Universal pre-fixes (run before per-language rules) ---
// Translators sometimes inserted spaces inside Liquid delimiters,
// breaking the tags (e.g. `{ % endif %}`, `{% endif % }`). Collapse
// these — but only when the tag has actual non-whitespace content
// inside, so we don't disturb the special `{% }` → `{% endif %}`
// recovery handled later. The English source never contains these
// patterns, so this is safe globally.
content = content.replace(/\{\s*%(-?)(\s*\S[^%]*?\s*)(-?)%\s*\}/g, '{%$1$2$3%}')
// Translators sometimes dropped the `data` keyword in front of a
// `variables.X.Y` / `reusables.X.Y` / `product.X` path. The English
// source never starts a Liquid tag with these prefixes; they always
// come inside `{% data variables.X %}` or similar. Restore the keyword.
content = content.replace(
/\{%(-?)\s+(variables|reusables)\.([A-Za-z0-9._-]+)(\s*-?%\})/g,
'{%$1 data $2.$3$4',
)
// `{% product.prodname_X %}` → `{% data variables.product.prodname_X %}`
content = content.replace(
/\{%(-?)\s+(product\.[A-Za-z0-9._-]+)(\s*-?%\})/g,
'{%$1 data variables.$2$3',
)
// Translators sometimes wrote `{% data.variables.X %}` (period instead
// of space) or `{% data.reusables.X %}` / `{% data.product.X %}`,
// which Liquid parses as a single variable lookup whose name starts
// with `.variables` / `.reusables`. Restore the space.
content = content.replace(
/\{%(-?)\s*data\.(variables|reusables)\.([A-Za-z0-9._-]+)(\s*-?%\})/g,
'{%$1 data $2.$3$4',
)
content = content.replace(
/\{%(-?)\s*data\.(product\.[A-Za-z0-9._-]+)(\s*-?%\})/g,
'{%$1 data variables.$2$3',
)
// The translation pipeline frequently splits Markdown bullet markers
// (`*`) and table-cell pipes (`|`) onto their own line, with the
// actual content pushed to the next line as deeply indented text.
// This breaks list and table rendering and leaves `[AUTOTITLE]` links
// unexpanded. Rejoin the marker with its content. This corruption
// affects every translated language (~47k bullets and ~11k cells in
// total), so it lives in the universal pre-fixes block.
content = content.replace(/^([ \t]*)\* ?\n[ \t]+/gm, '$1* ')
content = content.replace(/^\|[ \t]*\n[ \t]+/gm, '| ')
// The same translator wrapping habit also strands heading markers
// (`#`/`##`/...), blockquote markers (`>`), and the opening `**` of a
// bold span on their own line, with the actual content pushed to the
// next line as deeply indented text. This breaks heading/blockquote/
// bold rendering and leaves Liquid tags and `[AUTOTITLE]` links
// unexpanded. Rejoin them. Fence- and frontmatter-aware so we don't
// disturb fenced markdown examples or YAML frontmatter.
// ~3k headings, ~1.6k blockquotes, ~3.5k bold-after-marker cases
// measured across all eight translated languages.
content = joinDanglingMarkers(content)
// YAML `|2-` block-scalar artifacts: some translated frontmatter fields
// (typically `intro`) arrive with a spurious leading newline followed by
// deep indentation when the translator wrote `field: |2-\n\n content`.
// The YAML parser preserves the leading blank line and extra indentation
// in the parsed string. Strip that leading whitespace when the English
// source has no such prefix.
if (content.startsWith('\n') && !englishContent.startsWith('\n')) {
content = content.replace(/^\n[ \t]*/, '')
}
// --- Per-language fixes (es, ja, pt, zh, ru, fr, ko, de) ---
if (context.code === 'es') {
// Remove colon prefix on Liquid tags: `{%:` → `{%`
content = content.replace(/\{%:/g, '{%')
// `{% siVersion X %}` — Spanish "si" (if) fused with "Version" = ifversion
content = content.replaceAll('{% siVersion ', '{% ifversion ')
content = content.replaceAll('{%- siVersion ', '{%- ifversion ')
content = content.replaceAll('{% vulnerables variables.', '{% data variables.')
content = content.replaceAll('{% datos variables', '{% data variables')
content = content.replaceAll('{% de datos variables', '{% data variables')
content = content.replaceAll('{% datos reusables', '{% data reusables')
// `{% WORD de datos variables.` — extra Spanish word before "de datos variables"
// e.g. `{% uso de datos variables.` ("use of data variables") or
// `{% análisis de datos variables.` ("data analysis variables").
// Unicode-aware character class so accented translator words match.
content = content.replace(
/\{%(-?)\s*[\p{L}\p{M}]+\s+de datos (variables|reusables)\./gu,
'{%$1 data $2.',
)
// `{% de datos WORD variables.` — adjective inserted between "de datos" and path
// e.g. `{% de datos específico variables.` ("specific data variables")
content = content.replace(
/\{%(-?)\s*de datos [\p{L}\p{M}]+ (variables|reusables)\./gu,
'{%$1 data $2.',
)
// `{% WORD de variables.` — word + "de variables" (missing "datos" keyword)
// e.g. `{% alerta de variables.product.X %}` (alert of variables)
content = content.replace(
/\{%(-?)\s*[\p{L}\p{M}]+\s+de\s+(variables|reusables)\./gu,
'{%$1 data $2.',
)
content = content.replaceAll('{% data reutilizables.', '{% data reusables.')
// `{% datos reutilizables.` — fully translated "data reusables" path
content = content.replaceAll('{% datos reutilizables.', '{% data reusables.')
// `{% datos repositorios.` — translated "repositories" path segment
content = content.replaceAll('{% datos repositorios.', '{% data reusables.repositories.')
// `{% datos de variables.` — reversed word order with extra "de"
content = content.replaceAll('{% datos de variables.', '{% data variables.')
// `{% variables de datos.` — reversed word order "variables of data"
content = content.replaceAll('{% variables de datos.', '{% data variables.')
// `{% Datos ` — capitalized "datos" = data
content = content.replaceAll('{% Datos variables', '{% data variables')
// `{% dato ` — singular form of "datos" = data
content = content.replaceAll('{% dato variables', '{% data variables')
// Translated Liquid keywords
content = content.replaceAll('{% comentario %}', '{% comment %}')
content = content.replaceAll('{%- comentario %}', '{%- comment %}')
content = content.replaceAll('{% si ', '{% if ')
content = content.replaceAll('{% sin procesar %}', '{% raw %}')
content = content.replaceAll('{% %} sin procesar', '{% raw %}')
// "sin formato" is another translation of "raw"
content = content.replace(/\{%\s*%?sin formato\s*\}/g, '{% raw %}')
content = content.replaceAll(
'{% para glosario en glosarios %}',
'{% for glossary in glossaries %}',
)
content = content.replaceAll('{{ glosario.term }}', '{{ glossary.term }}')
content = content.replaceAll('{{ glosario.description }}', '{{ glossary.description }}')
// Catch "o" and "y/o" between any plan names in ifversion/elsif/if tags
content = content.replace(
/\{%-? (?:ifversion|elsif|if) [^%]*?(?:\by\/o\b|\bo\b)[^%]*?%\}/g,
(match) => {
return match.replace(/ y\/o /g, ' or ').replace(/ o /g, ' or ')
},
)
// Spanish "no" for "not" in ifversion/elsif/if tags
content = content.replace(/\{%-? (?:ifversion|elsif|if) [^%]*?\bno\b[^%]*?%\}/g, (match) => {
return match.replace(/ no /g, ' not ')
})
// Translated for-loop keywords
content = content.replace(/\{%-? para (?:la )?entrada en /g, (match) => {
return match.replace(/para (?:la )?entrada en/, 'for entry in')
})
// `{% para el modelo en X %}` — "for the model in" = for model in
content = content.replace(/\{%-? para el modelo en /g, (match) => {
return match.replace('para el modelo en', 'for model in')
})
content = content.replace(/\{%-? cuando /g, (match) => {
return match.replace('cuando', 'when')
})
// `{% icono "X" ... %}` — "icono" = "icon" = octicon
content = content.replaceAll('{% icono ', '{% octicon ')
content = content.replaceAll('{%- icono ', '{%- octicon ')
// `{% octicon "bombilla" %}` — Spanish "bombilla" = "light-bulb" (translated octicon name)
content = content.replaceAll('{% octicon "bombilla"', '{% octicon "light-bulb"')
content = content.replaceAll('{%- octicon "bombilla"', '{%- octicon "light-bulb"')
// `{% capturar X %}` — "capturar" = "to capture" = capture
content = content.replaceAll('{% capturar ', '{% capture ')
content = content.replaceAll('{%- capturar ', '{%- capture ')
// Translated block tags
content = content.replaceAll('{% nota %}', '{% note %}')
content = content.replaceAll('{%- nota %}', '{%- note %}')
content = content.replaceAll('{%- nota -%}', '{%- note -%}')
// `{% otra %}` / `{%- otra %}` — "another/other" = else
content = content.replaceAll('{% otra %}', '{% else %}')
content = content.replaceAll('{%- otra %}', '{%- else %}')
// `{% encabezados de fila %}` — "row headers" = rowheaders
content = content.replaceAll('{% encabezados de fila %}', '{% rowheaders %}')
content = content.replaceAll('{%- encabezados de fila %}', '{%- rowheaders %}')
// Spanish `o` = "or", `y` = "and" inside ifversion/elsif/if
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?\so\s[^%]*?-?%\}/g, (m) =>
m.replace(/\so\s/g, ' or '),
)
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?\sy\s[^%]*?-?%\}/g, (m) =>
m.replace(/\sy\s/g, ' and '),
)
// `{% ifversion ghes}` (missing `%` before `}`) — translator dropped the
// closing percent. Match plan name (fpt|ghec|ghes|ghae) followed by `}`
// not `%}`, immediately followed by content (so we don't over-match).
content = content.replace(
/\{%-?(\s+(?:ifversion|elsif|if)\s+(?:not\s+)?(?:fpt|ghec|ghes|ghae)(?:\s+(?:or|and)\s+(?:not\s+)?(?:fpt|ghec|ghes|ghae))*)\}/g,
'{%$1 %}',
)
// [SCRAPE-6548] Per-file fix for the Spanish reusable
// `data/reusables/dependency-graph/deduplication.md`. The translation
// dropped the `{% endif %}` after the Dependabot graph jobs item (the
// English source has it, scoped to fpt/ghec). Restore it so the outer
// `{% ifversion fpt or ghec %}` block balances. Scoped by both
// `dottedPath` (production reusable rendering via get-data.ts) and
// `relativePath` (count-translation-corruptions.ts validation path).
if (
context.dottedPath === 'reusables.dependency-graph.deduplication' ||
context.relativePath?.endsWith('data/reusables/dependency-graph/deduplication.md')
) {
content = content.replace(
'tienen prioridad sobre el envío automático de dependencias.\n',
'tienen prioridad sobre el envío automático de dependencias.{% endif %}\n',
)
}
}
if (context.code === 'ja') {
content = content.replaceAll('{% データ variables', '{% data variables')
content = content.replaceAll('{% データvariables', '{% data variables')
content = content.replaceAll('{% データ reusables', '{% data reusables')
content = content.replaceAll('{% データ変数.', '{% data variables.')
content = content.replaceAll('{% データ再利用可能な.', '{% data reusables.')
content = content.replaceAll('{% データ再利用可能.', '{% data reusables.')
content = content.replaceAll('{% データ再利用.', '{% data reusables.')
content = content.replaceAll('{% メモ %}', '{% note %}')
content = content.replaceAll('{%- メモ %}', '{%- note %}')
// Double-brace corruption of `{% data`: `{% {{データ}} variables.` → `{% data variables.`
content = content.replaceAll('{{データ}} variables.', 'data variables.')
// Catch "または" between any plan names in ifversion/elsif tags
content = content.replace(/\{%-? (?:ifversion|elsif) [^%]*?または[^%]*?%\}/g, (match) => {
return match.replace(/ または /g, ' or ')
})
// Fix trailing quote on YAML value
content = content.replace(/^(\s*asked_too_many_times:\s*.+)"\s*$/m, '$1')
// Fix Japanese nested Markdown links where the translation text
// inside parentheses confuses the Markdown parser. Inject a hair
// space (\u200A) between `]` and `(` so the parser treats them as
// separate tokens.
content = content.replace(/\[(\[.*?\])(\(\S+\)\]\()/g, '[$1\u200A$2')
// Translated Liquid keywords in case/when/comment/endcomment statements
content = content.replaceAll('{%- それ以外の場合 %}', '{%- else %}')
content = content.replaceAll('{% それ以外の場合 %}', '{% else %}')
content = content.replaceAll('{%- エンドケース -%}', '{%- endcase -%}')
content = content.replaceAll('{% エンドケース %}', '{% endcase %}')
content = content.replaceAll('{%- コメント %}', '{%- comment %}')
content = content.replaceAll('{% コメント %}', '{% comment %}')
content = content.replaceAll('{%- 終了コメント %}', '{%- endcomment %}')
content = content.replaceAll('{% 終了コメント %}', '{% endcomment %}')
content = content.replaceAll('{% エンドビジュアルスタジオ %}', '{% endvisualstudio %}')
content = content.replaceAll('{% エクリプス %}', '{% eclipse %}')
// `{% それ以外の %}` — truncated form of "in the other case" = else
content = content.replaceAll('{% それ以外の %}', '{% else %}')
content = content.replaceAll('{%- それ以外の %}', '{%- else %}')
// `{%- それ以外 %}` — further-truncated form (missing の/場合) = else
content = content.replaceAll('{% それ以外 %}', '{% else %}')
content = content.replaceAll('{%- それ以外 %}', '{%- else %}')
// `{% それ以外の場合 ifversion X %}` → `{% elsif X %}` (confused elsif + ifversion)
content = content.replace(/\{% それ以外の場合 ifversion\s+(.+?)\s*%\}/g, '{% elsif $1 %}')
// `{%- "supported" %}` → `{%- when "supported" %}` (missing `when`)
// Preserves original trim syntax (`{%-` vs `{%`)
content = content.replace(/\{%-?\s*"(supported|not_supported|preview)"\s*%\}/g, (match) => {
return match.replace(/(%-?)\s*"/, '$1 when "')
})
content = content.replace(
/\{%-?\s*"(サポートされている|サポートされていません|プレビュー)"\s*%\}/g,
(match) => {
return match
.replace('サポートされている', 'supported')
.replace('サポートされていません', 'not_supported')
.replace('プレビュー', 'preview')
.replace(/(%-?)\s*"/, '$1 when "')
},
)
// Empty trim tag `{%- %}C` → `{%- when "closing-down" %}C` (translation stripped `when "closing-down"`)
content = content.replaceAll('{%- %}C', '{%- when "closing-down" %}C')
// Deeply translated Liquid for-loops in table-generation templates.
// `{%- COLLECTION の VARNAME -%}` → `{%- for VARNAME in COLLECTION -%}`
// Covers `tables.X`, `groupVersions`, `ideEntry.features`, etc.
content = content.replace(
/\{%-?\s*([\w.]+(?:\[[\w"']+\])?)\s+の\s+(\w+)\s*-?%\}/g,
(match, collectionPath, varName) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
const closeDash = match.endsWith('-%}') ? '-%}' : '%}'
return `${dash} for ${varName} in ${collectionPath} ${closeDash}`
},
)
// `{%- COLLECTION %} の VARNAME の場合` → `{%- for VARNAME in COLLECTION %}`
// Variant where の and variable name appear OUTSIDE the tag close
content = content.replace(
/\{%-?\s*([\w.]+(?:\[[\w"']+\])?)\s*-?%\}\s+の(\w+)の場合/g,
(match, collectionPath, varName) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
return `${dash} for ${varName} in ${collectionPath} %}`
},
)
// `{{ バージョン }}` → `{{ version }}`
content = content.replaceAll('{{ バージョン }}', '{{ version }}')
content = content.replaceAll('{{ 言語 }}', '{{ language }}')
// `{%- 言語を割り当てる = X %}` → `{%- assign language = X %}`
content = content.replace(/\{%-?\s*言語を割り当てる\s*=\s*/g, (match) =>
match.startsWith('{%-') ? '{%- assign language = ' : '{% assign language = ',
)
// `{%- featureData = X %} を割り当てる` → `{%- assign featureData = X %}`
// and similar `= X %} を割り当てる` patterns
content = content.replace(
/\{%-?\s*(\w+)\s*=\s*([^%]+?)%\}\s*を割り当てる/g,
(match, varName, value) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
return `${dash} assign ${varName} = ${value.trim()} %}`
},
)
// `{%- ... -%} の割り当て` (stray "assignment of" after a tag) → strip it
content = content.replaceAll(' の割り当て', '')
// `{%- ... -%} の場合` ("in the case of" = if) → strip, the `if` is already in the tag
content = content.replaceAll(' の場合', '')
// Missing `if` in condition checks: `{%- featureData.X %}` → `{%- if featureData.X %}`
content = content.replace(
/\{%-?\s*((?:featureData|supportLevel|languageData|entry)\.\w+)\s*-?%\}/g,
(match, condition) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
const closeDash = match.endsWith('-%}') ? '-%}' : '%}'
return `${dash} if ${condition} ${closeDash}`
},
)
// Missing `assign` in assignments: `{%- varName = value %}` (no trailing keyword)
content = content.replace(
/\{%-?\s*(featureKey|featureData|supportLevel|languageData|groupName|groupVersions)\s*=\s*([^%]+?)-?%\}/g,
(match, varName, value) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
const closeDash = match.endsWith('-%}') ? '-%}' : '%}'
return `${dash} assign ${varName} = ${value.trim()} ${closeDash}`
},
)
// `{% 行ヘッダー %}` — "row headers" = rowheaders
content = content.replaceAll('{% 行ヘッダー %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行ヘッダー %}', '{%- rowheaders %}')
// `{% ウィンドウズ %}` — "Windows" = windows (platform tag)
content = content.replaceAll('{% ウィンドウズ %}', '{% windows %}')
content = content.replaceAll('{%- ウィンドウズ %}', '{%- windows %}')
// `{% ウィンドウ %}` — "Window" (without ズ suffix) = windows (alternate transliteration)
content = content.replaceAll('{% ウィンドウ %}', '{% windows %}')
content = content.replaceAll('{%- ウィンドウ %}', '{%- windows %}')
// `{% デスクトップ %}` — "desktop" (Japanese transliteration) = desktop platform tag
content = content.replaceAll('{% デスクトップ %}', '{% desktop %}')
content = content.replaceAll('{%- デスクトップ %}', '{%- desktop %}')
// `{%データ` (no space after `{%`) — also catches `{%データvariables`
content = content.replaceAll('{%データvariables', '{% data variables')
content = content.replaceAll('{%データ variables', '{% data variables')
content = content.replaceAll('{%- データvariables', '{%- data variables')
content = content.replaceAll('{%- データ variables', '{%- data variables')
content = content.replaceAll('{%- データ reusables', '{%- data reusables')
// `{% データ` followed by `.` (path operator) — translator dropped `variables`/`reusables`
content = content.replaceAll('{% データ.variables.', '{% data variables.')
content = content.replaceAll('{% データ.reusables.', '{% data reusables.')
// Generic Japanese `データ` data-tag normalizer.
// Matches `{%[-]?[ ]?データ[ ]?[再利用可能|再利用|変数|reusables|variables|...].PATH %}`
// and rewrites to `{%[-]? data <variables|reusables>.PATH %}` based on the keyword.
content = content.replace(
/\{%(-?)\s*データ\s*(再利用可能な?|再利用|reusables)\.([^\s%]+)\s*(-?)%\}/g,
(_m, dashOpen, _kw, path, dashClose) => `{%${dashOpen} data reusables.${path} ${dashClose}%}`,
)
content = content.replace(
/\{%(-?)\s*データ\s*(変数|variables)\.([^\s%]+)\s*(-?)%\}/g,
(_m, dashOpen, _kw, path, dashClose) => `{%${dashOpen} data variables.${path} ${dashClose}%}`,
)
// Bare `{%データ` / `{%- データ` followed by space + (variables|reusables)
content = content.replace(
/\{%(-?)\s*データ\s+(variables|reusables)\.([^\s%]+)\s*(-?)%\}/g,
(_m, dashOpen, kw, path, dashClose) => `{%${dashOpen} data ${kw}.${path} ${dashClose}%}`,
)
// `{% メモ` capitalized variant
content = content.replaceAll('{% メモ -%}', '{%- note -%}')
content = content.replaceAll('{%- メモ -%}', '{%- note -%}')
// `{% ノート %}` — alternate Japanese for "note"
content = content.replaceAll('{% ノート %}', '{% note %}')
content = content.replaceAll('{%- ノート %}', '{%- note %}')
// `{% 終わり %}` / `{% 終了 %}` — Japanese "end" used as endif
content = content.replaceAll('{% 終わり %}', '{% endif %}')
content = content.replaceAll('{%- 終わり %}', '{%- endif %}')
content = content.replaceAll('{% 終了 %}', '{% endif %}')
content = content.replaceAll('{%- 終了 %}', '{%- endif %}')
// `{% 終了for %}` / `{% endforの場合 %}` — endfor variants
content = content.replaceAll('{% 終了for %}', '{% endfor %}')
content = content.replaceAll('{%- 終了for %}', '{%- endfor %}')
// Japanese `または` = "or", `かつ` / `および` = "and" inside ifversion/elsif/if
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?または[^%]*?-?%\}/g, (m) =>
m.replace(/\s*または\s*/g, ' or '),
)
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?かつ[^%]*?-?%\}/g, (m) =>
m.replace(/\s*かつ\s*/g, ' and '),
)
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?および[^%]*?-?%\}/g, (m) =>
m.replace(/\s*および\s*/g, ' and '),
)
// `{% 行ヘッダー %}` — "row headers" = rowheaders
content = content.replaceAll('{% 行ヘッダー %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行ヘッダー %}', '{%- rowheaders %}')
// `{% 終了行ヘッダー %}` — "end row headers" = endrowheaders
content = content.replaceAll('{% 終了行ヘッダー %}', '{% endrowheaders %}')
content = content.replaceAll('{%- 終了行ヘッダー %}', '{%- endrowheaders %}')
// `{% ウィンドウ %}` / `{% ウィンドウズ %}` — "window/windows" = windows
content = content.replaceAll('{% ウィンドウ %}', '{% windows %}')
content = content.replaceAll('{%- ウィンドウ %}', '{%- windows %}')
content = content.replaceAll('{% ウィンドウズ %}', '{% windows %}')
content = content.replaceAll('{%- ウィンドウズ %}', '{%- windows %}')
// `{% Windowsターミナル %}` / `{% Windows ターミナル %}` — Windows terminal
content = content.replaceAll('{% Windowsターミナル %}', '{% windows %}')
content = content.replaceAll('{% Windows ターミナル %}', '{% windows %}')
// `{% indented_data_reference 再利用可能.X.Y spaces=N %}` — translated path
content = content.replace(/(\{%-?\s*indented_data_reference\s+)再利用可能\./g, '$1reusables.')
// [SCRAPE-6548] Per-file fixes for ja pages whose intro/title/shortTitle
// Liquid was structurally scrambled (orphan endif, swapped tag order,
// unclosed ifversion). Each replacement is scoped by the unique broken
// substring in the source field and rewrites only the broken Liquid; the
// Japanese prose is preserved exactly. These run only when context.code is
// 'ja' so they cannot affect other languages.
// admin/managing-iam/iam-configuration-reference/index.md (intro): orphan
// `{% endif %}` injected before `{% ifversion ghec %}` — drop it.
content = content.replaceAll(
'{% data variables.location.product_location %}{% endif %} の認証 {% ifversion ghec %} および Enterprise {% elsif ghes %} のプロビジョニングの構成についての参照情報を表示できます。',
'{% data variables.location.product_location %} の認証 {% ifversion ghec %} および Enterprise {% elsif ghes %} のプロビジョニングの構成{% endif %} についての参照情報を表示できます。',
)
// admin/managing-iam/configuring-authentication-for-enterprise-managed-users/configuring-saml-single-sign-on-with-okta-for-enterprise-managed-users.md
// (intro): `{% ifversion ghec %}` opens but never closes. Append `{% endif %}`.
content = content.replaceAll(
'{% ifversion ghec %}{% data variables.product.prodname_dotcom_the_website %} または {% data variables.enterprise.data_residency_site %} で、{% data variables.product.prodname_emus %} の Okta を構成する方法を説明します。',
'{% ifversion ghec %}{% data variables.product.prodname_dotcom_the_website %} または {% data variables.enterprise.data_residency_site %} で、{% data variables.product.prodname_emus %} の Okta を構成する方法を説明します。{% endif %}',
)
// admin/managing-iam/provisioning-user-accounts-with-scim/index.md
// (title, shortTitle, intro): all three fields have endif/else/elsif/ifversion
// tags reordered so they don't parse. Replace each with a clean version.
content = content.replaceAll(
'SCIM{% endif %} を使用したエンタープライズ マネージド ユーザー{% else %} 向けのプロビジョニング アカウント{% ifversion ghec %}',
'{% ifversion ghec %} SCIM を使用したエンタープライズ マネージド ユーザー{% else %} SCIM 向けのプロビジョニング アカウント{% endif %}',
)
content = content.replaceAll(
'SCIM{% endif %} を使用して{% ifversion ghec %} マネージド ユーザー アカウント{% else %} アカウントをプロビジョニングする',
'{% ifversion ghec %} SCIM を使用して マネージド ユーザー アカウント{% else %} SCIM アカウントをプロビジョニングする{% endif %}',
)
content = content.replaceAll(
'{% data variables.location.product_location %}{% endif %} の {% data variables.enterprise.prodname_emu_enterprise %}{% elsif ghes %} のユーザー{% ifversion ghec %} に対してアカウントをプロビジョニングし、組織とチームのメンバーシップを管理する方法について説明します。',
'{% ifversion ghec %} {% data variables.enterprise.prodname_emu_enterprise %}{% elsif ghes %} {% data variables.location.product_location %} のユーザー{% endif %} に対してアカウントをプロビジョニングし、組織とチームのメンバーシップを管理する方法について説明します。',
)
// admin/managing-iam/provisioning-user-accounts-with-scim/configuring-scim-provisioning-for-users.md
// (title): tags reordered. Rewrite to a clean structure.
content = content.replaceAll(
'ユーザー{% endif %} を管理するためのエンタープライズ マネージド ユーザー{% else %} の SCIM プロビジョニング {% ifversion ghec %} の構成',
'{% ifversion ghec %} エンタープライズ マネージド ユーザー{% else %} ユーザー{% endif %} を管理するための SCIM プロビジョニングの構成',
)
// code-security/.../configuring-code-scanning-for-your-appliance.md (intro):
// `{% ifversion default-setup-self-hosted-runners-GHEC %}` opens but never
// closes within the field. Append a closing `{% endif %}`.
content = content.replaceAll(
'{% data variables.product.prodname_dotcom %} ホステッド ランナー{% ifversion default-setup-self-hosted-runners-GHEC %}なしのエンタープライズに対して {% data variables.product.prodname_code_scanning %} を有効化、構成、および無効化できます。 {% data variables.product.prodname_code_scanning_caps %} を使用すると、コードの脆弱性やエラーをスキャンできます。',
'{% data variables.product.prodname_dotcom %} ホステッド ランナー{% ifversion default-setup-self-hosted-runners-GHEC %}なしのエンタープライズに対して{% endif %} {% data variables.product.prodname_code_scanning %} を有効化、構成、および無効化できます。 {% data variables.product.prodname_code_scanning_caps %} を使用すると、コードの脆弱性やエラーをスキャンできます。',
)
}
if (context.code === 'pt') {
// `{%–` — en-dash (U+2013) used instead of hyphen in `{%-` trim modifier
content = content.replaceAll('{%–', '{%-')
content = content.replaceAll('{% dados variables', '{% data variables')
content = content.replaceAll('{% de dados variables', '{% data variables')
content = content.replaceAll('{% dados reusables', '{% data reusables')
// `{% dadosvariables` / `{% datavariables` — no space between "dados"/"data" and "variables"
content = content.replaceAll('{% dadosvariables', '{% data variables')
content = content.replaceAll('{%- dadosvariables', '{%- data variables')
content = content.replaceAll('{% datavariables', '{% data variables')
content = content.replaceAll('{%- datavariables', '{%- data variables')
// No space between `{%` and `datavariables` (translator dropped both spaces)
content = content.replaceAll('{%datavariables', '{% data variables')
content = content.replaceAll('{%-datavariables', '{%- data variables')
// `{% data variables.product. prodname_X %}` — stray space inside the dotted
// path, just after `.product.`. Liquid tokenizes the path as a single ident,
// so the extra space breaks the lookup. Restore.
content = content.replace(
/\{%(-?)\s*data\s+variables\.product\.\s+(prodname_[A-Za-z0-9_]+)/g,
'{%$1 data variables.product.$2',
)
// Fully translated reusables path: `{% dados reutilizáveis.X.Y %}` → `{% data reusables.X.Y %}`
content = content.replaceAll('{% dados reutilizáveis.', '{% data reusables.')
// Translated path segment inside reusables path: `repositórios` → `repositories`
content = content.replaceAll(
'{% data reusables.repositórios.',
'{% data reusables.repositories.',
)
content = content.replaceAll('{{% dados ', '{% data ')
content = content.replaceAll('{{% datas ', '{% data ')
content = content.replaceAll('{% senão %}', '{% else %}')
content = content.replaceAll('{%- senão %}', '{%- else %}')
content = content.replaceAll('{% mais %}', '{% else %}')
content = content.replaceAll('{%- mais %}', '{%- else %}')
content = content.replaceAll('{% se ', '{% if ')
content = content.replaceAll('{% atribuir ', '{% assign ')
content = content.replaceAll('{% %} bruto', '{% raw %}')
content = content.replaceAll('{% %de dados reusables.', '{% data reusables.')
content = content.replaceAll('{% %de dados variables.', '{% data variables.')
content = content.replaceAll('{% %móvel }', '{% mobile %}')
// `{% variáveis de dados.` — reversed word order for "data variables" in Portuguese
content = content.replaceAll('{% variáveis de dados.', '{% data variables.')
content = content.replaceAll('{% variáveis de dados ', '{% data variables ')
// `{% dados variáveis.` — alternate word order "data variables"
content = content.replaceAll('{% dados variáveis.', '{% data variables.')
// `{% janelas %}` — Portuguese "windows" = windows (platform tag)
content = content.replaceAll('{% janelas %}', '{% windows %}')
content = content.replaceAll('{%- janelas %}', '{%- windows %}')
// `{% observação %}` — Portuguese "note" = note
content = content.replaceAll('{% observação %}', '{% note %}')
content = content.replaceAll('{%- observação %}', '{%- note %}')
// `{% comentário %}` — Portuguese "comment" = comment
content = content.replaceAll('{% comentário %}', '{% comment %}')
content = content.replaceAll('{%- comentário %}', '{%- comment %}')
// `{% nota de fim %}` — Portuguese "end note" = endnote
content = content.replaceAll('{% nota de fim %}', '{% endnote %}')
content = content.replaceAll('{%- nota de fim %}', '{%- endnote %}')
// `{% Dados variables` — capitalized "Dados"
content = content.replaceAll('{% Dados variables', '{% data variables')
content = content.replaceAll('{%- Dados variables', '{%- data variables')
// Catch "ou" between any plan names in ifversion/elsif/if tags
content = content.replace(/\{%-? (?:ifversion|elsif|if) [^%]*?ou [^%]*?%\}/g, (match) => {
return match.replace(/ ou /g, ' or ')
})
// Fully translated reusable path in audit log article:
// `{% dados agrupados por categoria.complemento.audit_log.reference-grouped-by-category %}`
content = content.replaceAll(
'{% dados agrupados por categoria.complemento.audit_log.reference-grouped-by-category %}',
'{% data reusables.audit_log.reference-grouped-by-category %}',
)
// Portuguese decimal comma in version numbers inside ifversion/elsif tags: `3,16` → `3.16`
content = content.replace(/\{%-? (?:ifversion|elsif) [^%]*?%\}/g, (match) => {
return match.replace(/(\d),(\d)/g, '$1.$2')
})
// `{% para X em Y %}` — Portuguese "for X in Y"
content = content.replace(/\{%-? para (\w+) em /g, (match) => {
return match.replace(/para (\w+) em /, 'for $1 in ')
})
// `{% reutilizáveis.X.Y %}` — translated reusables path with no `data` prefix
content = content.replaceAll('{% reutilizáveis.', '{% data reusables.')
content = content.replaceAll('{%- reutilizáveis.', '{%- data reusables.')
// `{% dados reusáveis.X.Y %}` — alternate Portuguese spelling for "reusables"
content = content.replaceAll('{% dados reusáveis.', '{% data reusables.')
content = content.replaceAll('{%- dados reusáveis.', '{%- data reusables.')
// `{% reusáveis.X.Y %}` — alternate without `data` prefix
content = content.replaceAll('{% reusáveis.', '{% data reusables.')
content = content.replaceAll('{%- reusáveis.', '{%- data reusables.')
// `{% dados.reutilizáveis.X.Y %}` — translator used `.` instead of space between
// "dados" (data) and "reutilizáveis" (reusables)
content = content.replaceAll('{% dados.reutilizáveis.', '{% data reusables.')
content = content.replaceAll('{%- dados.reutilizáveis.', '{%- data reusables.')
// `{% dados.reusáveis.` — same with alternate spelling
content = content.replaceAll('{% dados.reusáveis.', '{% data reusables.')
content = content.replaceAll('{%- dados.reusáveis.', '{%- data reusables.')
// `{% de data X` — translator inserted Portuguese preposition "de" (of/from)
// before `data variables` / `data reusables`
content = content.replaceAll('{% de data variables', '{% data variables')
content = content.replaceAll('{%- de data variables', '{%- data variables')
content = content.replaceAll('{% de data reusables', '{% data reusables')
content = content.replaceAll('{%- de data reusables', '{%- data reusables')
content = content.replaceAll('{% de dados reusables', '{% data reusables')
// `{% datavariables` — no space between "data" and "variables" (sometimes survives)
content = content.replaceAll('{% datavariables', '{% data variables')
content = content.replaceAll('{%- datavariables', '{%- data variables')
// `{% datas variables` / `{% datas reusables` — plural Portuguese form of "data"
content = content.replaceAll('{% datas variables', '{% data variables')
content = content.replaceAll('{%- datas variables', '{%- data variables')
content = content.replaceAll('{% datas reusables', '{% data reusables')
content = content.replaceAll('{%- datas reusables', '{%- data reusables')
// Word-order swap inside ifversion: `{% ghes ifversion %}` → `{% ifversion ghes %}`
content = content.replace(
/\{%(-?)\s*(fpt|ghec|ghes)\s+ifversion\s*%\}/g,
'{%$1 ifversion $2 %}',
)
// With extra "de" word: `{% ghes de ifversion %}` → `{% ifversion ghes %}`
content = content.replace(
/\{%(-?)\s*(fpt|ghec|ghes)\s+de\s+ifversion\s*%\}/g,
'{%$1 ifversion $2 %}',
)
// Mangled order: `{% %} de ghec ifversion` → `{% ifversion ghec %}`
content = content.replaceAll('{% %} de ghec ifversion', '{% ifversion ghec %}')
content = content.replaceAll('{% %} de ghes ifversion', '{% ifversion ghes %}')
content = content.replaceAll('{% %} de fpt ifversion', '{% ifversion fpt %}')
// `{% referência_dados_indentados ` — Portuguese translation of `indented_data_reference`
content = content.replaceAll('{% referência_dados_indentados ', '{% indented_data_reference ')
content = content.replaceAll('{%- referência_dados_indentados ', '{%- indented_data_reference ')
// Broad fallback: any remaining `{% dados ` / `{% Dados ` → `{% data `
content = content.replace(/\{%(-?)\s*[Dd]ados\s+/g, '{%$1 data ')
// After broad fallback, translated path segments may remain. Catch the most common.
content = content.replace(/\{%(-?\s*)data reutilizáveis\./g, '{%$1data reusables.')
content = content.replace(/\{%(-?\s*)data variáveis\./g, '{%$1data variables.')
// `{% reutilizáveis.` / `{% variáveis.` (no `data` prefix) → add data
content = content.replace(/\{%(-?\s*)reutilizáveis\./g, '{%$1data reusables.')
content = content.replace(/\{%(-?\s*)variáveis\./g, '{%$1data variables.')
// Portuguese `ou` = "or" / `e` = "and" inside ifversion/elsif/if tags
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?\sou\s[^%]*?-?%\}/g, (m) =>
m.replace(/\sou\s/g, ' or '),
)
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?\se\s[^%]*?-?%\}/g, (m) =>
m.replace(/\se\s/g, ' and '),
)
// `{% senão %}` / `{% Senão %}` — Portuguese "else"
content = content.replaceAll('{% senão %}', '{% else %}')
content = content.replaceAll('{%- senão %}', '{%- else %}')
content = content.replaceAll('{% Senão %}', '{% else %}')
content = content.replaceAll('{% senao %}', '{% else %}')
content = content.replaceAll('{%- senao %}', '{%- else %}')
// `{% senão se ` / `{% senao se ` — "else if" = elsif
content = content.replaceAll('{% senão se ', '{% elsif ')
content = content.replaceAll('{%- senão se ', '{%- elsif ')
content = content.replaceAll('{% senao se ', '{% elsif ')
// `{% caso contrário %}` — alternate "otherwise" = else
content = content.replaceAll('{% caso contrário %}', '{% else %}')
content = content.replaceAll('{%- caso contrário %}', '{%- else %}')
// `{% observação %}` — "note" = note
content = content.replaceAll('{% observação %}', '{% note %}')
content = content.replaceAll('{%- observação %}', '{%- note %}')
// `{% modelo %}` / `{% modelo` — `template` (alias for `tool`)? Actually "modelo"
// appears as `{% modelo %}` orphaned. Drop unmatched bare `{% modelo %}` is
// risky; instead, leave as-is (Liquid will raise but rare).
// Per-file targeted fixes for translator-scrambled Liquid that we can't
// catch via generic patterns. These are scoped tightly to the originating
// file so they're a no-op everywhere else, and they touch only the
// already-broken Liquid fragments — translated prose is preserved.
//
// [SCRAPE-6548] migrating-between-github-products: intro had a stray space
// inside `{% data variables.product. prodname_ghe_cloud %}`. The generic
// pt regex above already restored it, but here we only need to confirm —
// no extra per-file replacement required.
}
if (context.code === 'zh') {
content = content.replaceAll('{% 数据variables', '{% data variables')
content = content.replaceAll('{% 数据 variables', '{% data variables')
// `{%数据variables` — no space between `{%` and 数据 (data)
content = content.replaceAll('{%数据variables', '{% data variables')
content = content.replaceAll('{%数据 variables', '{% data variables')
// Order matters: the more specific `s.` variant must run first to
// avoid the broader rule producing a double-s (`reusabless`).
content = content.replaceAll('{% 数据可重用s.', '{% data reusables.')
content = content.replaceAll('{% 数据可重用', '{% data reusables')
content = content.replaceAll('{% 其他 %}', '{% else %}')
content = content.replaceAll('{%- 其他 %}', '{%- else %}')
content = content.replaceAll('{% 原始 %}', '{% raw %}')
content = content.replaceAll('{%- 原始 %}', '{%- raw %}')
// `{% 否则 %}` — "otherwise" = else (different Chinese word than 其他)
content = content.replaceAll('{% 否则 %}', '{% else %}')
content = content.replaceAll('{%- 否则 %}', '{%- else %}')
// Chinese `如果` = "if": `{ 如果 X %}` → `{% if X %}`
content = content.replace(/\{ 如果 /g, '{% if ')
// Stray Chinese `,则为` ("then") merged with `{%` before HTML: `,则为 {%<tag>` → `<tag>`
// The regex consumes the `<` to avoid producing a double `<<`.
content = content.replace(/,则为 \{%</g, '<')
// Catch "或" / "和" between any plan names in ifversion/elsif/if tags
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?或[^%]*?-?%\}/g, (m) =>
m.replace(/\s*或\s*/g, ' or '),
)
content = content.replace(/\{%-?\s+(?:ifversion|elsif|if)\s+[^%]*?和[^%]*?-?%\}/g, (m) =>
m.replace(/\s*和\s*/g, ' and '),
)
// `{% 行标题 %}` — "row headers" = rowheaders
content = content.replaceAll('{% 行标题 %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行标题 %}', '{%- rowheaders %}')
// `{% 数据变量.` — "data variables" = data variables (with space before)
content = content.replaceAll('{% 数据变量.', '{% data variables.')
// `{%数据变量.` — same but no space between `{%` and 数据变量 (e.g. `{%数据变量.enterprise.management_console%}`)
content = content.replaceAll('{%数据变量.', '{% data variables.')
content = content.replaceAll('{%-数据变量.', '{%- data variables.')
// `{% Windows 操作系统 %}` — "Windows OS" = windows platform tag
content = content.replaceAll('{% Windows 操作系统 %}', '{% windows %}')
content = content.replaceAll('{%- Windows 操作系统 %}', '{%- windows %}')
// `{% Windows终端 %}` — "Windows terminal" = windows platform tag
content = content.replaceAll('{% Windows终端 %}', '{% windows %}')
// `{% 桌面 %}` — Chinese "desktop" = desktop platform tag
content = content.replaceAll('{% 桌面 %}', '{% desktop %}')
content = content.replaceAll('{%- 桌面 %}', '{%- desktop %}')
// `{% 行标头 %}` / `{% 行标题 %}` — alternate Chinese for "row headers"
content = content.replaceAll('{% 行标头 %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行标头 %}', '{%- rowheaders %}')
content = content.replaceAll('{% 行标题 %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行标题 %}', '{%- rowheaders %}')
// `{% 结束行标题 %}` / `{% 结束行标头 %}` / `{% 结束行头 %}` — endrowheaders
content = content.replaceAll('{% 结束行标题 %}', '{% endrowheaders %}')
content = content.replaceAll('{%- 结束行标题 %}', '{%- endrowheaders %}')
content = content.replaceAll('{% 结束行标头 %}', '{% endrowheaders %}')
content = content.replaceAll('{%- 结束行标头 %}', '{%- endrowheaders %}')
content = content.replaceAll('{% 结束行头 %}', '{% endrowheaders %}')
content = content.replaceAll('{%- 结束行头 %}', '{%- endrowheaders %}')
// `{% 行标题结束 %}` — order swap (rowheaders + end)
content = content.replaceAll('{% 行标题结束 %}', '{% endrowheaders %}')
content = content.replaceAll('{%- 行标题结束 %}', '{%- endrowheaders %}')
// Capitalized `{% Variables.X %}` / `{% Reusables.X %}` — translator title-cased
content = content.replaceAll('{% data Variables.', '{% data variables.')
content = content.replaceAll('{% data Reusables.', '{% data reusables.')
content = content.replaceAll('{%- data Variables.', '{%- data variables.')
content = content.replaceAll('{%- data Reusables.', '{%- data reusables.')
// `{% 否则如果 ` — "otherwise if" = elsif
content = content.replaceAll('{% 否则如果 ', '{% elsif ')
content = content.replaceAll('{%- 否则如果 ', '{%- elsif ')
// `{% 结束 %}` / `{% 结尾 %}` — Chinese "end" = endif
content = content.replaceAll('{% 结束 %}', '{% endif %}')
content = content.replaceAll('{%- 结束 %}', '{%- endif %}')
content = content.replaceAll('{% 结尾 %}', '{% endif %}')
content = content.replaceAll('{%- 结尾 %}', '{%- endif %}')
// `{% 结束for %}` — end + for
content = content.replaceAll('{% 结束for %}', '{% endfor %}')
content = content.replaceAll('{%- 结束for %}', '{%- endfor %}')
// `{% 结束if %}` / `{% endif的话 %}` — endif variants
content = content.replaceAll('{% 结束if %}', '{% endif %}')
content = content.replaceAll('{%- 结束if %}', '{%- endif %}')
// Broad fallback: any remaining `{% 数据 ` → `{% data `
content = content.replace(/\{%(-?)\s*数据\s+/g, '{%$1 data ')
// `{% indented_data_reference 可重用|可复用|可重用项|可重用组件|可复用项.X.Y spaces=N %}`
// — translator converted the `reusables` path prefix into Chinese. Collapse
// any `可(重|复)用[项|组件|s]?.` prefix into `reusables.`.
content = content.replace(
/(\{%-?\s*indented_data_reference\s+)可(?:重|复)用(?:项|组件|s)?\./g,
'$1reusables.',
)
// [SCRAPE-6548] Per-file fixes for zh pages whose Liquid was structurally
// scrambled. Each replacement uses the unique broken substring as a
// discriminator so it only fires for the right field of the right file.
// account-and-profile/concepts/username-changes.md (intro): orphan
// `{% endif %}` and `{% ifversion ghes %}` swapped — drop both.
content = content.replaceAll(
'如果实例使用内置身份验证{% endif %},则可以更改 {% data variables.product.github %} 帐户 {% ifversion ghes %} 的用户名。',
'可以更改 {% data variables.product.github %} 帐户的用户名。{% ifversion ghes %} 如果实例使用内置身份验证。{% endif %}',
)
// admin/managing-iam/using-saml-for-enterprise-iam/index.md (intro):
// three `{% ifversion %}` opens against one `{% endif %}`. Rebalance.
content = content.replaceAll(
'可以通过 SAML 单点登录 (SSO){% ifversion ghec %}和跨域身份管理系统 (SCIM){% endif %} 集中管理 {% ifversion ghes %} 帐户以及对 {% ifversion ghes %}{% data variables.location.product_location %}{% elsif ghec %}你的企业资源{% endif %}的访问权限。',
'可以通过 SAML 单点登录 (SSO){% ifversion ghec %}和跨域身份管理系统 (SCIM){% endif %} 集中管理帐户以及对 {% ifversion ghes %}{% data variables.location.product_location %}{% elsif ghec %}你的企业资源{% endif %}的访问权限。',
)
// code-security/.../configuring-access-to-private-registries-for-dependabot.md
// (intro): `{% ifversion dependabot-on-actions-self-hosted %}` opens but
// never closes. Append `{% endif %}`.
content = content.replaceAll(
'可以将身份验证信息(如密码和访问令牌)存储为加密机密,然后在配置文件中 {% data variables.product.prodname_dependabot %} 引用这些信息。{% ifversion dependabot-on-actions-self-hosted %} 如果您在专用网络上有注册表,您也可以在使用自托管运行程序执行{% data variables.product.prodname_dependabot %}时配置{% data variables.product.prodname_dependabot %}访问权限。',
'可以将身份验证信息(如密码和访问令牌)存储为加密机密,然后在配置文件中 {% data variables.product.prodname_dependabot %} 引用这些信息。{% ifversion dependabot-on-actions-self-hosted %} 如果您在专用网络上有注册表,您也可以在使用自托管运行程序执行{% data variables.product.prodname_dependabot %}时配置{% data variables.product.prodname_dependabot %}访问权限。{% endif %}',
)
// authentication/keeping-your-account-and-data-secure/security-log-events.md
// (markdown body line 15): the `> *` bullet has a duplicate
// `{% ifversion ghes %}` after the outer `{% ifversion ghes %}` block
// already opened on the previous line. Drop the inner duplicate so the
// outer endif balances correctly.
content = content.replaceAll('> * {% ifversion ghes %} 本文包含', '> * 本文包含')
}
if (context.code === 'ru') {
content = content.replaceAll('[«AUTOTITLE»](', '[AUTOTITLE](')
content = content.replaceAll('[АВТОЗАГОЛОВОК](', '[AUTOTITLE](')
// `[{% autoTITLE](url)` — Liquid-embedded lowercase autotitle (translator lowercased
// the link anchor and wrapped it in Liquid tag syntax instead of plain `[AUTOTITLE](url)`)
content = content.replaceAll('[{% autoTITLE](', '[AUTOTITLE](')
content = content.replaceAll('{% данных variables', '{% data variables')
content = content.replaceAll('{% данных, variables', '{% data variables')
content = content.replaceAll('{% данными variables', '{% data variables')
content = content.replaceAll('{% данных организации variables', '{% data variables')
content = content.replaceAll('{% данным variables.', '{% data variables.')
content = content.replaceAll('{% данные variables.', '{% data variables.')
content = content.replaceAll('{% данных reusables', '{% data reusables')
content = content.replaceAll('{% данные reusables', '{% data reusables')
content = content.replaceAll('{% данных переменных.', '{% data variables.')
// Broaden `{% данных.X` → `{% data variables.X` (covers .product., .dependency-review., .code-scanning., etc.)
content = content.replaceAll('{% данных.', '{% data variables.')
content = content.replaceAll('{% data переменных.', '{% data variables.')
content = content.replaceAll('{% переменным данных.', '{% data variables.')
// Broader "переменных данных" pattern — covers .dependency-review, .code-scanning, etc.
content = content.replaceAll('{% переменных данных.', '{% data variables.')
// Dot-prefix paths where `data variables` was entirely dropped
content = content.replaceAll('{% .dependency-review.', '{% data variables.dependency-review.')
content = content.replaceAll('{% .code-scanning.', '{% data variables.code-scanning.')
// Same without space after `{%`
content = content.replaceAll('{%.dependency-review.', '{% data variables.dependency-review.')
content = content.replaceAll('{%.code-scanning.', '{% data variables.code-scanning.')
content = content.replaceAll('{%.copilot.', '{% data variables.copilot.')
// Stray `"` between `данных` and `variables`
content = content.replaceAll('{% данных" variables', '{% data variables')
content = content.replaceAll('{%" variables.', '{% data variables.')
// Stray `,` replacing `data`
content = content.replaceAll('{%, variables.', '{% data variables.')
content = content.replaceAll('{% необработанного %}', '{% raw %}')
content = content.replaceAll('{%- необработанного %}', '{%- raw %}')
content = content.replaceAll('{%- ifversion fpt или ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt или ghec %}', '{% ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion ghec или fpt %}', '{% ifversion ghec or fpt %}')
content = content.replaceAll('{% ghes или ghec %}', '{% ifversion ghes or ghec %}')
content = content.replaceAll('{% elsif ghec или ghes %}', '{% elsif ghec or ghes %}')
// Catch remaining "или" between any plan names in ifversion/elsif/if tags
content = content.replace(/\{%-? (?:ifversion|elsif|if) [^%]*?или[^%]*?%\}/g, (match) => {
return match.replace(/ или /g, ' or ')
})
// Russian decimal comma in version numbers inside ifversion/elsif tags: `3,18` → `3.18`
content = content.replace(/\{%-? (?:ifversion|elsif) [^%]*?%\}/g, (match) => {
return match.replace(/(\d),(\d)/g, '$1.$2')
})
content = content.replaceAll('{% конечным %}', '{% endif %}')
content = content.replaceAll('{%- конечным %}', '{%- endif %}')
// `{%- конец %}` — dash-trimmed form of "end" = endif
content = content.replaceAll('{%- конец %}', '{%- endif %}')
// `{%- конец для %}` — "end for" = endfor
content = content.replaceAll('{%- конец для %}', '{%- endfor %}')
// `{% заголовки строк %}` — "row headers" = rowheaders (opener; `{% endrowheaders %}` stays in English)
content = content.replaceAll('{% заголовки строк %}', '{% rowheaders %}')
content = content.replaceAll('{%- заголовки строк %}', '{%- rowheaders %}')
// `{% windowsTerminal %}` — "Windows Terminal" platform tag with capital T
// (the correct tag name is lowercase `{% windowsterminal %}`)
content = content.replaceAll('{% windowsTerminal %}', '{% windowsterminal %}')
content = content.replaceAll('{%- windowsTerminal %}', '{%- windowsterminal %}')
// `{%- командная палитра ifversion %}` — "command palette ifversion" with word order swapped
// Russian "командная палитра" (command palette) was placed before "ifversion" and the
// feature-flag arg was dropped. Recover as `{%- ifversion command-palette %}`.
content = content.replace(
/\{%(-?)\s*командная\s+палитра\s+ifversion\s*(-?)%\}/g,
'{%$1 ifversion command-palette $2%}',
)
// `{% конец %}` after `{% raw %}` means `{% endraw %}`, not `{% endif %}`.
// Handle this BEFORE the generic `{% конец %}` → `{% endif %}` fallback.
// We use a split-based approach instead of `[^]*?` regex to avoid
// catastrophic backtracking on large content (~20s on 150KB inputs).
if (content.includes('{% конец %}') && content.includes('{% raw %}')) {
const parts = content.split('{% raw %}')
for (let i = 1; i < parts.length; i++) {
parts[i] = parts[i].replace('{% конец %}', '{% endraw %}')
}
content = parts.join('{% raw %}')
}
content = content.replaceAll('{% конец %}', '{% endif %}')
// Cyrillic transliteration of `elsif` (lossy → else, since version param is lost)
content = content.replaceAll('{% Эльсиф %}', '{% else %}')
content = content.replaceAll('{%- Эльсиф %}', '{%- else %}')
// Translated feature flag names
content = content.replaceAll(
'обязательный-2fa-dotcom-участник',
'mandatory-2fa-dotcom-contributors',
)
content = content.replaceAll(
'обязательный-2fa-участник-2023',
'mandatory-2fa-contributors-2023',
)
// `не` = "not" in ifversion tags
content = content.replaceAll('{% ifversion не ', '{% ifversion not ')
content = content.replaceAll('{% переменных данных.', '{% data variables.')
content = content.replaceAll('{% повторно используемых данных.', '{% data reusables.')
content = content.replaceAll('{% примечание %}', '{% note %}')
content = content.replaceAll('{%- примечание %}', '{%- note %}')
content = content.replaceAll('{% конечных головщиков %}', '{% endrowheaders %}')
content = content.replaceAll('{% данных для повторного использования.', '{% data reusables.')
// `{% indented_data_reference повторн... .X.Y spaces=N %}` — translator
// converted the `reusables` path prefix into Russian (with spaces inside),
// which breaks the indented_data_reference parser. Collapse any
// `повторн[...]<word>[ <word>]*.` prefix into `reusables.`.
content = content.replace(
/(\{%-?\s*indented_data_reference\s+)повторн[а-яё]*(?:\s+[а-яё]+)*\./g,
'$1reusables.',
)
content = content.replaceAll('{% еще %}', '{% else %}')
content = content.replaceAll('{%- еще %}', '{%- else %}')
content = content.replaceAll('{% ещё %}', '{% else %}')
content = content.replaceAll('{%- ещё %}', '{%- else %}')
// `{% иначе %}` — "otherwise" = else
content = content.replaceAll('{% иначе %}', '{% else %}')
content = content.replaceAll('{%- иначе %}', '{%- else %}')
content = content.replaceAll('{% необработанные %}', '{% raw %}')
content = content.replaceAll('{%- необработанные %}', '{%- raw %}')
content = content.replaceAll('{% необработанный %}', '{% raw %}')
content = content.replaceAll('{%- необработанный %}', '{%- raw %}')
content = content.replaceAll('{% сырой %}', '{% raw %}')
content = content.replaceAll('{%- сырой %}', '{%- raw %}')
content = content.replaceAll('{% нарисовать %}', '{% endraw %}')
content = content.replaceAll('{%- нарисовать %}', '{%- endraw %}')
content = content.replaceAll('{% эндкёрл %}', '{% endcurl %}')
content = content.replaceAll('{%- эндкёрл %}', '{%- endcurl %}')
content = content.replaceAll('{% запроса %}', '{% endraw %}')
content = content.replaceAll('{%- запроса %}', '{%- endraw %}')
// `{% джетмозги %}` — Russian literal translation of "JetBrains" (джет=jet, мозги=brains)
content = content.replaceAll('{% джетмозги %}', '{% jetbrains %}')
content = content.replaceAll('{%- джетмозги %}', '{%- jetbrains %}')
// Russian translation of github-glossary.md
content = content.replaceAll(
'{% для глоссария в глоссариях %}',
'{% for glossary in glossaries %}',
)
content = content.replaceAll('{{ глоссарий.term }}', '{{ glossary.term }}')
content = content.replaceAll('{{ глоссарий.description }}', '{{ glossary.description }}')
// Rearranged `{% data VARIABLE_PATH %}` → `VARIABLE_PATH %данн... {% }`
// The translation moved `data` (as `данных`/`данными`/`данные`) after the path
// and split `%}` into `{% }` or `{% }`. Reconstruct the original tag.
// Guard: these regexes start with [\w.-]+ which backtracks O(n²) on large word-char strings.
if (content.includes('%данн')) {
content = content.replace(
/([\w.-]+\.[\w.-]+\.[\w_]+) %данн\w*[^{]*\{%\s+\}/g,
'{% data $1 %}',
)
content = content.replace(
/([\w.-]+\.[\w.-]+\.[\w_]+) %\}данн\w*\s*\{%\s*\./g,
'{% data $1 %}.',
)
}
if (content.includes('%{% data')) {
// Variant where path precedes `%{% data }`: `PATH %{% data }.`
content = content.replace(/([\w.-]+\.[\w.-]+\.[\w_]+) %\{% data\s+\}/g, '{% data $1 %}')
}
// Translated octicon names
content = content.replaceAll(
'{% octicon "организация" aria-hidden="true" aria-label="organization" %}',
'{% octicon "organization" aria-hidden="true" aria-label="organization" %}',
)
// `{% Эльсиф CONDITION %}` — transliteration of "elsif" with a condition
content = content.replace(/\{%(-?)\s*Эльсиф\s+/g, '{%$1 elsif ')
// `{% для X в Y %}` — Russian "for X in Y"
content = content.replace(/\{%-?\s*для\s+(\w+)\s+в\s+/g, (match) => {
const dash = match.startsWith('{%-') ? '{%-' : '{%'
return match.replace(/^\{%-?\s*для\s+(\w+)\s+в\s+/, `${dash} for $1 in `)
})
// `, а не ghes` — Russian "and not ghes" inside ifversion expressions
content = content.replace(/\{%-? (?:ifversion|elsif|if) [^%]*?, а не [^%]*?%\}/g, (match) => {
return match.replace(/, а не /g, ' and not ')
})
// `{% ifversion ghes не ` — `не` ("not") inside ifversion
content = content.replace(/\{%-? (?:ifversion|elsif|if) [^%]*?\sне\s[^%]*?%\}/g, (match) => {
return match.replace(/ не /g, ' not ')
})
// `aria-label="autoTITLE"` — "AUTOTITLE" was lowercased by translator
content = content.replaceAll('aria-label="autoTITLE"', 'aria-label="AUTOTITLE"')
// `{% эндраw %}` / `{% эндраw -%}` — transliterated endraw
content = content.replaceAll('{% эндраw %}', '{% endraw %}')
content = content.replaceAll('{%- эндраw %}', '{%- endraw %}')
content = content.replaceAll('{% эндраw -%}', '{% endraw -%}')
// `{% эндесктоп %}` — transliterated enddesktop
content = content.replaceAll('{% эндесктоп %}', '{% enddesktop %}')
content = content.replaceAll('{%- эндесктоп %}', '{%- enddesktop %}')
// `{% эндекклипс %}` / `{% эндеклипс %}` — transliterated endeclipse
content = content.replaceAll('{% эндеклипс %}', '{% endeclipse %}')
content = content.replaceAll('{%- эндеклипс %}', '{%- endeclipse %}')
content = content.replaceAll('{% эндекклипс %}', '{% endeclipse %}')
// `{% endекклипс %}` — partial transliteration
content = content.replaceAll('{% endекклипс %}', '{% endeclipse %}')
// `{%- лицензия %}` — Russian "license"... actually this is a feature flag value, not a tag
// Translator-formatted "Russian smart quotes" inside Liquid tags: «X» → "X"
content = content.replace(/(\{%-?\s*[a-z]+\s+)«([^»]*)»/g, '$1"$2"')
// `{% ifversion fpt or ghec or ghes >NUMBER %}` — when range value is wrapped in
// Cyrillic chars or letter "о" instead of "0", normalize digits
content = content.replace(/\{%-? (?:ifversion|elsif) [^%]*?[<>=][^%]*?%\}/g, (match) => {
// Cyrillic 'о' (U+043E) often replaces ASCII '0' (U+0030)
return match.replace(/(\d)\s*о/g, '$10').replace(/о\s*(\d)/g, '0$1')
})
// Word-order swap: translator placed plan name BEFORE `ifversion`, e.g.
// `{% ghes ifversion %}` → `{% ifversion ghes %}`,
// `{% ghes ifversion < 3,14 %}` → `{% ifversion ghes < 3.14 %}`
content = content.replace(
/\{%(-?)\s*(fpt|ghec|ghes|ghae|ghecom)\s+ifversion\s*([^%]*?)\s*-?%\}/g,
(_m, dash, plan, rest) => {
const fixedRest = rest.replace(/(\d),(\d)/g, '$1.$2')
const trimmed = fixedRest.trim()
return `{%${dash} ifversion ${plan}${trimmed ? ` ${trimmed}` : ''} %}`
},
)
// Missing `ifversion` prefix: `{% ghes или ghec %}` → `{% ifversion ghes or ghec %}`
content = content.replace(
/\{%(-?)\s*(fpt|ghec|ghes|ghae|ghecom)\s+или\s+(fpt|ghec|ghes|ghae|ghecom)\s*-?%\}/g,
'{%$1 ifversion $2 or $3 %}',
)
// Same pattern with "and" / "и"
content = content.replace(
/\{%(-?)\s*(fpt|ghec|ghes|ghae|ghecom)\s+и\s+(fpt|ghec|ghes|ghae|ghecom)\s*-?%\}/g,
'{%$1 ifversion $2 and $3 %}',
)
// `{% ghes version %}` (translator dropped `ifversion`, added "version")