scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
240 lines
7.5 KiB
Dart
240 lines
7.5 KiB
Dart
/// Generates lib/data/iso_639_data.dart from the curated offline ISO 639 catalog.
|
|
///
|
|
/// Usage:
|
|
/// dart run scripts/codegen/generate_iso_639_data.dart [input.json] [output.dart]
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
const defaultIso639Input = 'scripts/codegen/data/iso_639_codes.json';
|
|
const defaultIso639Output = 'lib/data/iso_639_data.dart';
|
|
|
|
final class Iso639CatalogEntry {
|
|
const Iso639CatalogEntry({
|
|
required this.primary,
|
|
required this.terminology,
|
|
required this.bibliographic,
|
|
required this.name,
|
|
});
|
|
|
|
final String primary;
|
|
final String terminology;
|
|
final String? bibliographic;
|
|
final String name;
|
|
}
|
|
|
|
final class Iso639Catalog {
|
|
const Iso639Catalog({required this.entries});
|
|
|
|
final List<Iso639CatalogEntry> entries;
|
|
}
|
|
|
|
typedef AtomicFileWriter = Future<void> Function(String path, String contents);
|
|
|
|
Iso639Catalog parseIso639Catalog(String source) {
|
|
final Object? decoded;
|
|
try {
|
|
decoded = jsonDecode(source);
|
|
} on FormatException catch (error) {
|
|
throw FormatException('Invalid ISO 639 catalog JSON: ${error.message}');
|
|
}
|
|
|
|
final root = _expectMap(decoded, 'catalog');
|
|
if (root['schemaVersion'] != 1) {
|
|
throw const FormatException('ISO 639 catalog schemaVersion must be 1');
|
|
}
|
|
|
|
final languages = _expectMap(root['languages'], 'languages');
|
|
if (languages.isEmpty) {
|
|
throw const FormatException('ISO 639 catalog languages must not be empty');
|
|
}
|
|
|
|
final entries = <Iso639CatalogEntry>[];
|
|
final allCodes = <String>{};
|
|
String? previousPrimary;
|
|
|
|
for (final catalogEntry in languages.entries) {
|
|
final key = catalogEntry.key;
|
|
if (!_isPrimaryCode(key)) {
|
|
throw FormatException('Language key must be exactly two lowercase letters: $key');
|
|
}
|
|
if (previousPrimary != null && key.compareTo(previousPrimary) <= 0) {
|
|
throw FormatException('Language keys must be in strictly increasing order: $key');
|
|
}
|
|
previousPrimary = key;
|
|
|
|
final rawEntry = _expectMap(catalogEntry.value, 'languages.$key');
|
|
final primary = _expectString(rawEntry['primary'], 'languages.$key.primary');
|
|
if (primary != key) {
|
|
throw FormatException('Language key $key does not match primary code $primary');
|
|
}
|
|
if (!_isPrimaryCode(primary)) {
|
|
throw FormatException('languages.$key.primary must be exactly two lowercase letters');
|
|
}
|
|
if (!allCodes.add(primary)) {
|
|
throw FormatException('Duplicate ISO 639 code: $primary');
|
|
}
|
|
|
|
final terminology = _expectString(rawEntry['terminology'], 'languages.$key.terminology');
|
|
if (!_isAliasCode(terminology)) {
|
|
throw FormatException('languages.$key.terminology must be exactly three lowercase letters');
|
|
}
|
|
if (!allCodes.add(terminology)) {
|
|
throw FormatException('Duplicate ISO 639 code: $terminology');
|
|
}
|
|
|
|
final rawBibliographic = rawEntry['bibliographic'];
|
|
final String? bibliographic;
|
|
if (rawBibliographic == null) {
|
|
bibliographic = null;
|
|
} else {
|
|
bibliographic = _expectString(rawBibliographic, 'languages.$key.bibliographic');
|
|
if (!_isAliasCode(bibliographic)) {
|
|
throw FormatException('languages.$key.bibliographic must be null or exactly three lowercase letters');
|
|
}
|
|
if (!allCodes.add(bibliographic)) {
|
|
throw FormatException('Duplicate ISO 639 code: $bibliographic');
|
|
}
|
|
}
|
|
|
|
final name = _expectNonemptyString(rawEntry['name'], 'languages.$key.name');
|
|
entries.add(
|
|
Iso639CatalogEntry(primary: primary, terminology: terminology, bibliographic: bibliographic, name: name),
|
|
);
|
|
}
|
|
|
|
return Iso639Catalog(entries: List.unmodifiable(entries));
|
|
}
|
|
|
|
String renderIso639Data(Iso639Catalog catalog) {
|
|
final output = StringBuffer()
|
|
..writeln(
|
|
'// Generated by dart run scripts/codegen/generate_iso_639_data.dart from '
|
|
'scripts/codegen/data/iso_639_codes.json; do not edit by hand.',
|
|
)
|
|
..writeln()
|
|
..writeln('class LanguageEntry {')
|
|
..writeln(' final String code1;')
|
|
..writeln(' final String code2;')
|
|
..writeln(' final String? code2B;')
|
|
..writeln(' final String name;')
|
|
..writeln(' const LanguageEntry(this.code1, this.code2, this.code2B, this.name);')
|
|
..writeln('}')
|
|
..writeln()
|
|
..writeln('const languageEntries = <String, LanguageEntry>{');
|
|
|
|
for (final entry in catalog.entries) {
|
|
final bibliographic = entry.bibliographic == null ? 'null' : _dartString(entry.bibliographic!);
|
|
output.writeln(
|
|
' ${_dartString(entry.primary)}: LanguageEntry('
|
|
'${_dartString(entry.primary)}, ${_dartString(entry.terminology)}, $bibliographic, ${_dartString(entry.name)}),',
|
|
);
|
|
}
|
|
|
|
output
|
|
..writeln('};')
|
|
..writeln()
|
|
..writeln('const code2ToCode1 = <String, String>{');
|
|
for (final entry in catalog.entries) {
|
|
output.writeln(' ${_dartString(entry.terminology)}: ${_dartString(entry.primary)},');
|
|
}
|
|
|
|
output
|
|
..writeln('};')
|
|
..writeln()
|
|
..writeln('const code2BToCode1 = <String, String>{');
|
|
for (final entry in catalog.entries) {
|
|
final bibliographic = entry.bibliographic;
|
|
if (bibliographic != null) {
|
|
output.writeln(' ${_dartString(bibliographic)}: ${_dartString(entry.primary)},');
|
|
}
|
|
}
|
|
output.writeln('};');
|
|
return output.toString();
|
|
}
|
|
|
|
Future<void> writeFileAtomically(String path, String contents) async {
|
|
final output = File(path);
|
|
await output.parent.create(recursive: true);
|
|
final temporary = File('$path.tmp.$pid.${DateTime.now().microsecondsSinceEpoch}');
|
|
try {
|
|
await temporary.writeAsString(contents, flush: true);
|
|
await temporary.rename(path);
|
|
} finally {
|
|
if (await temporary.exists()) {
|
|
await temporary.delete();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> generateIso639Data(
|
|
String inputPath,
|
|
String outputPath, {
|
|
AtomicFileWriter writer = writeFileAtomically,
|
|
}) async {
|
|
final source = await File(inputPath).readAsString();
|
|
final catalog = parseIso639Catalog(source);
|
|
final rendered = renderIso639Data(catalog);
|
|
await writer(outputPath, rendered);
|
|
}
|
|
|
|
Future<void> main(List<String> arguments) async {
|
|
if (arguments.length > 2) {
|
|
stderr.writeln('Usage: dart run scripts/codegen/generate_iso_639_data.dart [input.json] [output.dart]');
|
|
exitCode = 64;
|
|
return;
|
|
}
|
|
|
|
final inputPath = arguments.isEmpty ? defaultIso639Input : arguments[0];
|
|
final outputPath = arguments.length < 2 ? defaultIso639Output : arguments[1];
|
|
await generateIso639Data(inputPath, outputPath);
|
|
}
|
|
|
|
Map<String, Object?> _expectMap(Object? value, String path) {
|
|
if (value is! Map<String, Object?>) {
|
|
throw FormatException('$path must be a JSON object');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
String _expectString(Object? value, String path) {
|
|
if (value is! String) {
|
|
throw FormatException('$path must be a string');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
String _expectNonemptyString(Object? value, String path) {
|
|
final string = _expectString(value, path);
|
|
if (string.trim().isEmpty) {
|
|
throw FormatException('$path must not be empty');
|
|
}
|
|
return string;
|
|
}
|
|
|
|
bool _isPrimaryCode(String code) => RegExp(r'^[a-z]{2}$').hasMatch(code);
|
|
bool _isAliasCode(String code) => RegExp(r'^[a-z]{3}$').hasMatch(code);
|
|
|
|
String _dartString(String value) {
|
|
if (value.contains("'")) {
|
|
final escaped = value
|
|
.replaceAll(r'\', r'\\')
|
|
.replaceAll('"', r'\"')
|
|
.replaceAll(r'$', r'\$')
|
|
.replaceAll('\r', r'\r')
|
|
.replaceAll('\n', r'\n')
|
|
.replaceAll('\t', r'\t');
|
|
return '"$escaped"';
|
|
}
|
|
|
|
final escaped = value
|
|
.replaceAll(r'\', r'\\')
|
|
.replaceAll("'", r"\'")
|
|
.replaceAll(r'$', r'\$')
|
|
.replaceAll('\r', r'\r')
|
|
.replaceAll('\n', r'\n')
|
|
.replaceAll('\t', r'\t');
|
|
return "'$escaped'";
|
|
}
|