mirror of
https://github.com/immich-app/immich.git
synced 2025-12-24 01:11:32 +03:00
365 lines
8.6 KiB
Dart
365 lines
8.6 KiB
Dart
// 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<String, dynamic>;
|
|
|
|
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<String, TranslationNode> children;
|
|
final List<TranslationParam> params;
|
|
|
|
const TranslationNode({
|
|
required this.key,
|
|
this.value,
|
|
Map<String, TranslationNode>? children,
|
|
List<TranslationParam>? 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<void> _generateTranslations(Map<String, dynamic> 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<String, Object>? 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<String, dynamic>) {
|
|
final children = <String, TranslationNode>{};
|
|
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<TranslationParam> _extractParams(String value) {
|
|
final params = <String, TranslationParam>{};
|
|
|
|
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<TranslationParam> 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 = <String>[];
|
|
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;
|
|
}
|