// ignore_for_file: avoid_print import 'dart:convert'; import 'dart:io'; const _kReservedWords = [ 'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'covariant', 'default', 'deferred', 'do', 'dynamic', 'else', 'enum', 'export', 'extends', 'extension', 'external', 'factory', 'false', 'final', 'finally', 'for', 'Function', 'get', 'hide', 'if', 'implements', 'import', 'in', 'interface', 'is', 'late', 'library', 'mixin', 'new', 'null', 'on', 'operator', 'part', 'required', 'rethrow', 'return', 'sealed', 'set', 'show', 'static', 'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var', 'void', 'when', 'while', 'with', 'yield', ]; const _kIntParamNames = [ 'count', 'number', 'amount', 'total', 'index', 'size', 'length', 'width', 'height', 'year', 'month', 'day', 'hour', 'minute', 'second', 'page', 'limit', 'offset', 'max', 'min', 'id', 'num', 'quantity', ]; void main() async { final sourceFile = File('../i18n/en.json'); if (!await sourceFile.exists()) { stderr.writeln('Source file does not exist'); return; } final outputDir = Directory('lib/generated'); await outputDir.create(recursive: true); final content = await sourceFile.readAsString(); final translations = json.decode(content) as Map; final outputFile = File('lib/generated/translations.g.dart'); await _generateTranslations(translations, outputFile); print('Generated ${outputFile.path}'); } class TranslationNode { final String key; final String? value; final Map children; final List params; const TranslationNode({ required this.key, this.value, Map? children, List? params, }) : children = children ?? const {}, params = params ?? const []; bool get isLeaf => value != null; bool get hasParams => params.isNotEmpty; } class TranslationParam { final String name; final String type; const TranslationParam(this.name, this.type); } Future _generateTranslations(Map translations, File output) async { final root = _buildTranslationTree('', translations); final buffer = StringBuffer(''' // DO NOT EDIT. This is code generated via generate_keys.dart import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/message_format.dart'; extension TranslationsExtension on BuildContext { Translations get t => Translations.of(this); } class StaticTranslations { StaticTranslations._(); static final instance = Translations._(null); } abstract class _BaseTranslations { BuildContext? get _context; String _t(String key, [Map? args]) { if (key.isEmpty) return ''; try { final translated = key.tr(context: _context); return args != null ? MessageFormat(translated, locale: Intl.defaultLocale ?? 'en').format(args) : translated; } catch (e) { return key; } } } class Translations extends _BaseTranslations { @override final BuildContext? _context; Translations._(this._context); static Translations of(BuildContext context) { context.locale; return Translations._(context); } '''); _generateClassMembers(buffer, root, ' '); buffer.writeln('}'); _generateNestedClasses(buffer, root); await output.writeAsString(buffer.toString()); } TranslationNode _buildTranslationTree(String key, dynamic value) { if (value is Map) { final children = {}; for (final entry in value.entries) { children[entry.key] = _buildTranslationTree(entry.key, entry.value); } return TranslationNode(key: key, children: children); } else { final stringValue = value.toString(); final params = _extractParams(stringValue); return TranslationNode(key: key, value: stringValue, params: params); } } List _extractParams(String value) { final params = {}; final icuRegex = RegExp(r'\{(\w+),\s*(plural|select|number|date|time)([^}]*(?:\{[^}]*\}[^}]*)*)\}'); for (final match in icuRegex.allMatches(value)) { final name = match.group(1)!; final icuType = match.group(2)!; final icuContent = match.group(3) ?? ''; if (params.containsKey(name)) continue; String type; if (icuType == 'plural' || icuType == 'number') { type = 'int'; } else if (icuType == 'select') { final hasTrueFalse = RegExp(r',\s*(true|false)\s*\{').hasMatch(icuContent); type = hasTrueFalse ? 'bool' : 'String'; } else { type = 'String'; } params[name] = TranslationParam(name, type); } var cleanedValue = value; var depth = 0; var icuStart = -1; for (var i = 0; i < value.length; i++) { if (value[i] == '{') { if (depth == 0) icuStart = i; depth++; } else if (value[i] == '}') { depth--; if (depth == 0 && icuStart >= 0) { final block = value.substring(icuStart, i + 1); if (RegExp(r'^\{\w+,').hasMatch(block)) { cleanedValue = cleanedValue.replaceFirst(block, ''); } icuStart = -1; } } } final simpleRegex = RegExp(r'\{(\w+)\}'); for (final match in simpleRegex.allMatches(cleanedValue)) { final name = match.group(1)!; if (params.containsKey(name)) continue; String type; if (_kIntParamNames.contains(name.toLowerCase())) { type = 'int'; } else { type = 'Object'; } params[name] = TranslationParam(name, type); } return params.values.toList(); } void _generateClassMembers(StringBuffer buffer, TranslationNode node, String indent, [String keyPrefix = '']) { final sortedKeys = node.children.keys.toList()..sort(); for (final childKey in sortedKeys) { final child = node.children[childKey]!; final dartName = _escapeName(childKey); final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; if (child.isLeaf) { if (child.hasParams) { _generateMethod(buffer, dartName, fullKey, child.params, indent); } else { _generateGetter(buffer, dartName, fullKey, indent); } } else { final className = _toNestedClassName(keyPrefix, childKey); buffer.writeln('${indent}late final $dartName = $className._(_context);'); } } } void _generateGetter(StringBuffer buffer, String dartName, String translationKey, String indent) { buffer.writeln('${indent}String get $dartName => _t(\'$translationKey\');'); } void _generateMethod( StringBuffer buffer, String dartName, String translationKey, List params, String indent, ) { final paramList = params.map((p) => 'required ${p.type} ${_escapeName(p.name)}').join(', '); final argsMap = params.map((p) => '\'${p.name}\': ${_escapeName(p.name)}').join(', '); buffer.writeln('${indent}String $dartName({$paramList}) => _t(\'$translationKey\', {$argsMap});'); } void _generateNestedClasses(StringBuffer buffer, TranslationNode node, [String keyPrefix = '']) { final sortedKeys = node.children.keys.toList()..sort(); for (final childKey in sortedKeys) { final child = node.children[childKey]!; final fullKey = keyPrefix.isEmpty ? childKey : '$keyPrefix.$childKey'; if (!child.isLeaf && child.children.isNotEmpty) { final className = _toNestedClassName(keyPrefix, childKey); buffer.writeln(); buffer.writeln('class $className extends _BaseTranslations {'); buffer.writeln(' @override'); buffer.writeln(' final BuildContext? _context;'); buffer.writeln(' $className._(this._context);'); _generateClassMembers(buffer, child, ' ', fullKey); buffer.writeln('}'); _generateNestedClasses(buffer, child, fullKey); } } } String _toNestedClassName(String prefix, String key) { final parts = []; if (prefix.isNotEmpty) { parts.addAll(prefix.split('.')); } parts.add(key); final result = StringBuffer('_'); for (final part in parts) { final words = part.split('_'); for (final word in words) { if (word.isNotEmpty) { result.write(word[0].toUpperCase()); if (word.length > 1) { result.write(word.substring(1).toLowerCase()); } } } } result.write('Translations'); return result.toString(); } String _escapeName(String name) { if (_kReservedWords.contains(name)) { return '$name\$'; } if (RegExp(r'^[0-9]').hasMatch(name)) { return 'k$name'; } return name; }