/// migrates chosen strings from lemmy-translations into flutter's i18n solution /// uses prettier to format the files import 'dart:convert'; import 'dart:io'; import 'common.dart'; import 'gen_l10n_from_string.dart' as gen; /// Map, if `renamedKey` is null then no rename is performed const toExtract = { 'settings': null, 'password': null, 'email_or_username': null, 'posts': null, 'comments': null, 'modlog': null, 'community': null, 'url': null, 'title': null, 'body': null, 'nsfw': null, 'post': null, 'save': null, 'send_message': null, 'subscribed': null, 'local': null, 'all': null, 'replies': null, 'mentions': null, 'from': null, 'to': null, 'deleted': 'deleted_by_creator', 'more': null, 'mark_as_read': null, 'mark_as_unread': null, 'reply': null, 'edit': null, 'delete': null, 'restore': null, 'yes': null, 'no': null, 'avatar': null, 'banner': null, 'display_name': null, 'bio': null, 'email': null, 'matrix_user_id': 'matrix_user', 'sort_type': null, 'type': null, 'show_nsfw': null, 'send_notifications_to_email': null, 'delete_account': null, 'saved': null, 'communities': null, 'users': null, 'theme': null, 'language': null, 'hot': null, 'new': null, 'old': null, 'top': null, 'chat': null, 'admin': null, 'by': null, 'not_a_mod_or_admin': null, 'not_an_admin': null, 'couldnt_find_post': null, 'not_logged_in': null, 'site_ban': null, 'community_ban': null, 'downvotes_disabled': null, 'invalid_url': null, 'locked': null, 'couldnt_create_comment': null, 'couldnt_like_comment': null, 'couldnt_update_comment': null, 'no_comment_edit_allowed': null, 'couldnt_save_comment': null, 'couldnt_get_comments': null, 'report_reason_required': null, 'report_too_long': null, 'couldnt_create_report': null, 'couldnt_resolve_report': null, 'invalid_post_title': null, 'couldnt_create_post': null, 'couldnt_like_post': null, 'couldnt_find_community': null, 'couldnt_get_posts': null, 'no_post_edit_allowed': null, 'couldnt_save_post': null, 'site_already_exists': null, 'couldnt_update_site': null, 'invalid_community_name': null, 'community_already_exists': null, 'community_moderator_already_exists': null, 'community_follower_already_exists': null, 'not_a_moderator': null, 'couldnt_update_community': null, 'no_community_edit_allowed': null, 'system_err_login': null, 'community_user_already_banned': null, 'couldnt_find_that_username_or_email': null, 'password_incorrect': null, 'registration_closed': null, 'invalid_password': null, 'passwords_dont_match': null, 'captcha_incorrect': null, 'invalid_username': null, 'bio_length_overflow': null, 'couldnt_update_user': null, 'couldnt_update_private_message': null, 'couldnt_update_post': null, 'couldnt_create_private_message': null, 'no_private_message_edit_allowed': null, 'post_title_too_long': null, 'email_already_exists': null, 'user_already_exists': null, 'number_online': 'number_users_online', }; // TODO: migrate those with changed capitalization // 'number_of_comments': null, const repoName = 'lemmy-translations'; const baseLanguage = 'en'; const flutterIntlPrefix = 'intl_'; Future main(List args) async { final force = args.contains('-f') || args.contains('--force'); final repoCleanup = await cloneLemmyTranslations(); final lemmyTranslations = await loadLemmyStrings(); final lemmurTranslations = await loadLemmurStrings(); portStrings(lemmyTranslations, lemmurTranslations, force: force); await save(lemmurTranslations); await repoCleanup(); await Process.run( 'npx', ['prettier', 'lib/l10n/*.arb', '--parser', 'json', '--write']); await gen.main(args); } /// returns a cleanup function Future Function()> cloneLemmyTranslations() async { await Process.run('git', ['clone', 'https://github.com/LemmyNet/$repoName']); return () => Directory(repoName).delete(recursive: true); } /// Map> Future>> loadLemmyStrings() async { final translationsDir = Directory('$repoName/translations'); final translations = >{}; await for (final file in translationsDir.list()) { final transFile = File.fromUri(file.uri); final trans = Map.from( jsonDecode(await transFile.readAsString()) as Map, ); final localeName = file.uri.pathSegments.last.split('.json').first; translations[localeName] = trans; } return translations; } /// Map> + some metadata Future>> loadLemmurStrings() async { final translationsDir = Directory('lib/l10n'); final translations = >{}; await for (final file in translationsDir.list()) { if (!file.path.endsWith('.arb')) continue; final transFile = File.fromUri(file.uri); final trans = jsonDecode(await transFile.readAsString()) as Map; final localeName = file.uri.pathSegments.last .split('.arb') .first .split(flutterIntlPrefix) .last; translations[localeName] = trans; } return translations; } /// will port them into `lemmurTranslations` void portStrings( Map> lemmyTranslations, Map> lemmurTranslations, { bool force = false, }) { // port all languages for (final language in lemmyTranslations.keys) { if (!lemmurTranslations.containsKey(language)) { lemmurTranslations[language] = {}; lemmurTranslations[language]['@@locale'] = language; } } for (final extract in toExtract.entries) { final key = extract.key; final renamedKey = extract.value ?? key; if (!lemmyTranslations[baseLanguage].containsKey(key)) { printError('"$key" does not exist in $repoName'); } if (lemmurTranslations[baseLanguage].containsKey(renamedKey) && !force) { confirm('"$key" already exists in lemmur, overwrite?'); } final metadata = {}; // ignore: omit_local_variable_types String Function(Map translations) transformer = (translations) => translations[key]; // check if it has a plural form if (lemmyTranslations[baseLanguage].containsKey('${key}_plural')) { final variable = RegExp(r'{([\w_]+)}') .firstMatch(lemmyTranslations[baseLanguage][key]) .group(1); transformer = (translations) { if (translations[key] == null) return null; final fixedVariables = translations[key].replaceAll('{{$variable}}', '{$variable}'); final pluralForm = () { if (translations.containsKey('${key}_plural')) { return translations['${key}_plural'] .replaceAll('{{$variable}}', '{$variable}'); } return null; }(); return '{$variable,plural, =1{$fixedVariables}${pluralForm != null ? ' other{$pluralForm}' : ''}}'; }; metadata['placeholders'] = {variable: {}}; } for (final trans in lemmyTranslations.entries) { final language = trans.key; final strings = trans.value; lemmurTranslations[language][renamedKey] = transformer(strings); } lemmurTranslations[baseLanguage]['@$renamedKey'] = metadata; } } Future save(Map> lemmurTranslations) async { // remove null fields // Vec<(language, key)> final toRemove = >[]; for (final translations in lemmurTranslations.entries) { final language = translations.key; for (final strings in translations.value.entries) { if (strings.value == null) { toRemove.add([language, strings.key]); } } } for (final rem in toRemove) { lemmurTranslations[rem[0]].remove(rem[1]); } for (final language in lemmurTranslations.keys) { await File('lib/l10n/$flutterIntlPrefix$language.arb') .writeAsString(jsonEncode(lemmurTranslations[language])); } }