381 lines
10 KiB
Dart
381 lines
10 KiB
Dart
/// 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;
|
|
|
|
// config for migration of a single key
|
|
// ignore: camel_case_types
|
|
class _ {
|
|
final String key;
|
|
final String? rename;
|
|
|
|
/// make all letters except the first one lower case
|
|
final bool decapitalize;
|
|
final bool toLowerCase;
|
|
|
|
/// arb format for the placeholder
|
|
final String? format;
|
|
|
|
/// arb type for the placeholder
|
|
final String? type;
|
|
|
|
const _(
|
|
this.key, {
|
|
this.rename,
|
|
this.decapitalize = false,
|
|
this.toLowerCase = false,
|
|
this.format,
|
|
this.type,
|
|
});
|
|
|
|
String get renamedKey => rename ?? key;
|
|
|
|
// will transform a value of a translation of the base language
|
|
String transform(String input) {
|
|
if (toLowerCase) return input.toLowerCase();
|
|
|
|
if (decapitalize) return '${input[0]}${input.substring(1).toLowerCase()}';
|
|
|
|
return input;
|
|
}
|
|
}
|
|
|
|
const toMigrate = <_>[
|
|
_('settings'),
|
|
_('password'),
|
|
_('email_or_username'),
|
|
_('posts'),
|
|
_('comments'),
|
|
_('modlog'),
|
|
_('community'),
|
|
_('url'),
|
|
_('title'),
|
|
_('body'),
|
|
_('nsfw'),
|
|
_('post'),
|
|
_('save'),
|
|
_('subscribed'),
|
|
_('local'),
|
|
_('all'),
|
|
_('replies'),
|
|
_('mentions'),
|
|
_('from'),
|
|
_('to'),
|
|
_('deleted', rename: 'deleted_by_creator'),
|
|
_('more'),
|
|
_('mark_as_read'),
|
|
_('mark_as_unread'),
|
|
_('reply'),
|
|
_('edit'),
|
|
_('delete'),
|
|
_('restore'),
|
|
_('yes'),
|
|
_('no'),
|
|
_('avatar'),
|
|
_('banner'),
|
|
_('display_name'),
|
|
_('bio'),
|
|
_('email'),
|
|
_('matrix_user_id', rename: 'matrix_user'),
|
|
_('sort_type'),
|
|
_('type'),
|
|
_('show_nsfw'),
|
|
_('send_notifications_to_email'),
|
|
_('delete_account', decapitalize: true),
|
|
_('saved'),
|
|
_('communities'),
|
|
_('users'),
|
|
_('theme'),
|
|
_('language'),
|
|
_('hot'),
|
|
_('new', rename: 'new_'),
|
|
_('old'),
|
|
_('top'),
|
|
_('chat'),
|
|
_('admin'),
|
|
_('by'),
|
|
_('not_a_mod_or_admin'),
|
|
_('not_an_admin'),
|
|
_('couldnt_find_post'),
|
|
_('not_logged_in'),
|
|
_('site_ban'),
|
|
_('community_ban'),
|
|
_('downvotes_disabled'),
|
|
_('invalid_url'),
|
|
_('locked'),
|
|
_('couldnt_create_comment'),
|
|
_('couldnt_like_comment'),
|
|
_('couldnt_update_comment'),
|
|
_('no_comment_edit_allowed'),
|
|
_('couldnt_save_comment'),
|
|
_('couldnt_get_comments'),
|
|
_('report_reason_required'),
|
|
_('report_too_long'),
|
|
_('couldnt_create_report'),
|
|
_('couldnt_resolve_report'),
|
|
_('invalid_post_title'),
|
|
_('couldnt_create_post'),
|
|
_('couldnt_like_post'),
|
|
_('couldnt_find_community'),
|
|
_('couldnt_get_posts'),
|
|
_('no_post_edit_allowed'),
|
|
_('couldnt_save_post'),
|
|
_('site_already_exists'),
|
|
_('couldnt_update_site'),
|
|
_('invalid_community_name'),
|
|
_('community_already_exists'),
|
|
_('community_moderator_already_exists'),
|
|
_('community_follower_already_exists'),
|
|
_('not_a_moderator'),
|
|
_('couldnt_update_community'),
|
|
_('no_community_edit_allowed'),
|
|
_('system_err_login'),
|
|
_('community_user_already_banned'),
|
|
_('couldnt_find_that_username_or_email'),
|
|
_('password_incorrect'),
|
|
_('registration_closed'),
|
|
_('invalid_password'),
|
|
_('passwords_dont_match'),
|
|
_('captcha_incorrect'),
|
|
_('invalid_username'),
|
|
_('bio_length_overflow'),
|
|
_('couldnt_update_user'),
|
|
_('couldnt_update_private_message'),
|
|
_('couldnt_update_post'),
|
|
_('couldnt_create_private_message'),
|
|
_('no_private_message_edit_allowed'),
|
|
_('post_title_too_long'),
|
|
_('email_already_exists'),
|
|
_('user_already_exists'),
|
|
_('number_online', rename: 'number_of_users_online'),
|
|
_('number_of_comments', type: 'int', format: 'compact', toLowerCase: true),
|
|
_('number_of_posts', type: 'int', format: 'compact', toLowerCase: true),
|
|
_('number_of_subscribers'),
|
|
_('number_of_users'),
|
|
_('unsubscribe', toLowerCase: true),
|
|
_('subscribe', toLowerCase: true),
|
|
_('messages'),
|
|
_('banned_users', decapitalize: true),
|
|
_('delete_account_confirm'),
|
|
_('new_password', decapitalize: true),
|
|
_('verify_password', decapitalize: true),
|
|
_('old_password', decapitalize: true),
|
|
_('show_avatars', decapitalize: true),
|
|
_('search', toLowerCase: true),
|
|
_('send_message', decapitalize: true),
|
|
_('top_day'),
|
|
_('top_week'),
|
|
_('top_month'),
|
|
_('top_year'),
|
|
_('top_all'),
|
|
_('most_comments'),
|
|
_('new_comments'),
|
|
_('active'),
|
|
_('bot_account'),
|
|
_('show_bot_accounts'),
|
|
_('show_read_posts'),
|
|
];
|
|
|
|
const repoName = 'lemmy-translations';
|
|
const baseLanguage = 'en';
|
|
const flutterIntlPrefix = 'intl_';
|
|
final outDir = RegExp('^arb-dir: (.+)')
|
|
.firstMatch(File('l10n.yaml').readAsStringSync())!
|
|
.group(1)!;
|
|
|
|
Future<void> main(List<String> args) async {
|
|
final force = args.contains('-f') || args.contains('--force');
|
|
|
|
checkDuplicateKeys();
|
|
|
|
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',
|
|
'$outDir/*.arb',
|
|
'--parser',
|
|
'json',
|
|
'--write',
|
|
'--print-width',
|
|
'1',
|
|
]);
|
|
|
|
await gen.main(args);
|
|
}
|
|
|
|
/// check if `toMigrate` has duplicate keys
|
|
void checkDuplicateKeys() {
|
|
final seen = <String>{};
|
|
for (final renamedKey in toMigrate.map((e) => e.renamedKey)) {
|
|
if (seen.contains(renamedKey)) {
|
|
printError(
|
|
'The renamedKey "$renamedKey" appears more than once in "toMigrate"');
|
|
}
|
|
seen.add(renamedKey);
|
|
}
|
|
}
|
|
|
|
/// returns a cleanup function
|
|
Future<Future<void> Function()> cloneLemmyTranslations() async {
|
|
await Process.run('git', ['clone', 'https://github.com/LemmyNet/$repoName']);
|
|
|
|
return () => Directory(repoName).delete(recursive: true);
|
|
}
|
|
|
|
/// Map<languageTag, Map<stringKey, stringValue>>
|
|
Future<Map<String, Map<String, String>>> loadLemmyStrings() async {
|
|
final translationsDir = Directory('$repoName/translations');
|
|
final translations = <String, Map<String, String>>{};
|
|
|
|
await for (final file in translationsDir.list()) {
|
|
final transFile = File.fromUri(file.uri);
|
|
final trans = Map<String, String>.from(
|
|
jsonDecode(await transFile.readAsString()) as Map<String, dynamic>,
|
|
);
|
|
|
|
final localeName = file.uri.pathSegments.last.split('.json').first;
|
|
translations[localeName] = trans;
|
|
}
|
|
|
|
return translations;
|
|
}
|
|
|
|
/// Map<languageTag, Map<stringKey, stringValue>> + some metadata
|
|
Future<Map<String, Map<String, dynamic>>> loadLemmurStrings() async {
|
|
final translationsDir = Directory(outDir);
|
|
final translations = <String, Map<String, dynamic>>{};
|
|
|
|
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<String, dynamic>;
|
|
|
|
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<String, Map<String, String>> lemmyTranslations,
|
|
Map<String, Map<String, dynamic>> lemmurTranslations, {
|
|
bool force = false,
|
|
}) {
|
|
// port all languages
|
|
for (final language in lemmyTranslations.keys) {
|
|
if (!lemmurTranslations.containsKey(language)) {
|
|
lemmurTranslations[language] = {'@@locale': language};
|
|
}
|
|
}
|
|
|
|
final baseTranslations = lemmyTranslations[baseLanguage]!;
|
|
|
|
for (final migrate in toMigrate) {
|
|
if (!baseTranslations.containsKey(migrate.key)) {
|
|
printError('"${migrate.key}" does not exist in $repoName');
|
|
}
|
|
|
|
if (lemmurTranslations[baseLanguage]!.containsKey(migrate.renamedKey) &&
|
|
!force) {
|
|
confirm('"${migrate.key}" already exists in lemmur, overwrite?');
|
|
}
|
|
|
|
final variableName = RegExp(r'{{([\w_]+)}|')
|
|
.firstMatch(baseTranslations[migrate.key]!)
|
|
?.group(1);
|
|
|
|
final metadata = <String, dynamic>{
|
|
if (variableName != null)
|
|
'placeholders': {
|
|
variableName: {
|
|
if (migrate.type != null) 'type': migrate.type,
|
|
if (migrate.format != null) 'format': migrate.format,
|
|
},
|
|
},
|
|
};
|
|
// ignore: omit_local_variable_types
|
|
String? Function(Map<String, String> translations) transformer =
|
|
(translations) => translations[migrate.key];
|
|
|
|
// check if it has a plural form
|
|
if (baseTranslations.containsKey('${migrate.key}_plural')) {
|
|
transformer = (translations) {
|
|
if (translations[migrate.key] == null) return null;
|
|
|
|
final fixedVariables = translations[migrate.key]!
|
|
.replaceAll('{{$variableName}}', '{$variableName}');
|
|
|
|
final pluralForm = () {
|
|
if (translations.containsKey('${migrate.key}_plural')) {
|
|
return translations['${migrate.key}_plural']!
|
|
.replaceAll('{{$variableName}}', '{$variableName}');
|
|
}
|
|
|
|
return null;
|
|
}();
|
|
|
|
if (pluralForm == null) {
|
|
return '{$variableName,plural, other{$fixedVariables}}';
|
|
}
|
|
|
|
return '{$variableName,plural, =1{$fixedVariables} other{$pluralForm}}';
|
|
};
|
|
}
|
|
|
|
for (final trans in lemmyTranslations.entries) {
|
|
final language = trans.key;
|
|
final strings = trans.value;
|
|
|
|
lemmurTranslations[language]![migrate.renamedKey] = transformer(strings);
|
|
}
|
|
final transformed = transformer(baseTranslations);
|
|
if (transformed != null) {
|
|
lemmurTranslations[baseLanguage]![migrate.renamedKey] =
|
|
migrate.transform(transformed);
|
|
}
|
|
lemmurTranslations[baseLanguage]!['@${migrate.renamedKey}'] = metadata;
|
|
}
|
|
}
|
|
|
|
Future<void> save(Map<String, Map<String, dynamic>> lemmurTranslations) async {
|
|
// remove null fields
|
|
// Vec<(language, key)>
|
|
final toRemove = <List<String>>[];
|
|
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('$outDir/$flutterIntlPrefix$language.arb')
|
|
.writeAsString(jsonEncode(lemmurTranslations[language]));
|
|
}
|
|
}
|