174
click.secho(f'Updated file: {path}. {missed} comments need manual changes', fg='green' if missed == 0 else 'yellow', bold=True)
175
176
177
-def run(resource_folder: str, baseline_language: str, fix: bool):
177
+ADML_NS = '{http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions}'
178
+RESOURCE_FOLDER = 'localization/strings'
179
+BASELINE_LANGUAGE = 'en-US'
180
+ADML_FOLDER = 'intune'
181
+ADML_FILENAME = 'WSL.adml'
182
+
183
+def get_adml_entries(path: str) -> tuple[dict, set]:
184
+ """Parse an .adml file.
185
+
186
+ Returns ({string_id: (value, [locked_tokens])}, {presentation_id, ...}).
187
+ Locked tokens are extracted from XML comments of the form
188
+ `<!-- {Locked="..."}{Locked="..."} -->` placed immediately before a
189
+ `<string>` element. Non-Locked comments are ignored.
190
+ """
191
+ # Parse with a TreeBuilder that preserves comments so we can associate
192
+ # {Locked="..."} tokens with the <string> element that follows them.
193
+ parser = xml.etree.ElementTree.XMLParser(
194
+ target=xml.etree.ElementTree.TreeBuilder(insert_comments=True))
195
+ root = xml.etree.ElementTree.parse(path, parser=parser).getroot()
196
+
197
+ string_table = root.find(f'.//{ADML_NS}stringTable')
198
+ if string_table is None:
199
+ raise RuntimeError(f'error: {path} is missing the required <stringTable> element')
200
+
201
+ strings = {}
202
+ pending_tokens = []
203
+ for child in string_table:
204
+ if child.tag is xml.etree.ElementTree.Comment:
205
+ pending_tokens.extend(re.findall(r'\{Locked="([^"]*)"\}', child.text or ''))
206
+ elif child.tag == f'{ADML_NS}string':
207
+ sid = child.get('id')
208
+ if sid is not None:
209
+ strings[sid] = (child.text or '', pending_tokens)
210
+ pending_tokens = []
211
+ else:
212
+ pending_tokens = []
213
+
214
+ presentation_table = root.find(f'.//{ADML_NS}presentationTable')
215
+ if presentation_table is None:
216
+ raise RuntimeError(f'error: {path} is missing the required <presentationTable> element')
217
+
218
+ presentations = {p.get('id') for p in presentation_table.findall(f'{ADML_NS}presentation') if p.get('id')}
219
+
220
+ return strings, presentations
221
+
222
+def validate_adml(adml_folder: str, baseline_language: str) -> bool:
223
+ baseline_path = f'{adml_folder}/{baseline_language}/{ADML_FILENAME}'
224
+ if not os.path.isfile(baseline_path):
225
+ print(f'info: ADML baseline not found at {baseline_path}, skipping ADML validation')
226
+ return True
227
+
228
+ print(f'Validating ADML baseline {baseline_path}')
229
+ baseline, baseline_presentations = get_adml_entries(baseline_path)
230
+ baseline_ids = set(baseline.keys())
231
+
232
+ result = True
233
+ for sid, (value, tokens) in baseline.items():
234
+ for tok in tokens:
235
+ if tok not in value:
236
+ print(f'error: locked token "{tok}" not found in baseline ADML string {sid}: {value}')
237
+ result = False
238
+
239
+ if not os.path.isdir(adml_folder):
240
+ return result
241
+
242
+ for entry in sorted(os.listdir(adml_folder)):
243
+ locale_path = f'{adml_folder}/{entry}/{ADML_FILENAME}'
244
+ if entry == baseline_language or not os.path.isfile(locale_path):
245
+ continue
246
+
247
+ print(f'Validating ADML {locale_path}')
248
+ translated, translated_presentations = get_adml_entries(locale_path)
249
+
250
+ missing = baseline_ids - set(translated.keys())
251
+ extra = set(translated.keys()) - baseline_ids
252
+ if missing:
253
+ print(f'error: ADML {locale_path} is missing string ids: {sorted(missing)}')
254
+ result = False
255
+ if extra:
256
+ print(f'error: ADML {locale_path} has unexpected string ids: {sorted(extra)}')
257
+ result = False
258
+
259
+ missing_p = baseline_presentations - translated_presentations
260
+ extra_p = translated_presentations - baseline_presentations
261
+ if missing_p:
262
+ print(f'error: ADML {locale_path} is missing presentation ids: {sorted(missing_p)}')
263
+ result = False
264
+ if extra_p:
265
+ print(f'error: ADML {locale_path} has unexpected presentation ids: {sorted(extra_p)}')
266
+ result = False
267
+
268
+ for sid in baseline_ids & set(translated.keys()):
269
+ _, tokens = baseline[sid]
270
+ tvalue, _ = translated[sid]
271
+ for tok in tokens:
272
+ if tok not in tvalue:
273
+ print(f'error: locked token "{tok}" not found in {locale_path} string {sid}: {tvalue}')
274
+ result = False
275
+
276
+ return result
277
+
278
+def run(resource_folder: str, baseline_language: str, fix: bool, adml_folder: str):
279
baseline_file = f'{resource_folder}/{baseline_language}/Resources.resw'
280
281
strings = get_strings_from_file(baseline_file, True)
287
print(f'Validating inserts in {path}')
288
result &= validate_resource(baseline, path)
289
290
+ result &= validate_adml(adml_folder, baseline_language)
291
+
292
if fix and comments:
293
fix_comments(comments, baseline_file, strings)
294
296
297
298
if __name__ == '__main__':
196
- if len(sys.argv) == 3: # Hack to work around pip install errors in the build pipeline
197
- run(sys.argv[1], sys.argv[2], False)
299
+ if len(sys.argv) == 1: # Avoid pulling in click for the default (CI) invocation
300
+ run(RESOURCE_FOLDER, BASELINE_LANGUAGE, False, ADML_FOLDER)
301
else:
302
import click
303
304
@click.command()
202
- @click.argument('resource-folder', default='localization/strings')
203
- @click.argument('baseline-language', default='en-us')
305
+ @click.option('--resource-folder', default=RESOURCE_FOLDER, show_default=True)
306
+ @click.option('--baseline-language', default=BASELINE_LANGUAGE, show_default=True)
307
+ @click.option('--adml-folder', default=ADML_FOLDER, show_default=True)
308
@click.option('--fix', is_flag=True)
205
- def main(resource_folder: str, baseline_language: str, fix: bool):
206
- run(resource_folder, baseline_language, fix)
207
-
309
+ def main(resource_folder: str, baseline_language: str, adml_folder: str, fix: bool):
310
+ run(resource_folder, baseline_language, fix, adml_folder)
311
+
312
main()
\ No newline at end of file