diff --git a/README.md b/README.md index 4a049ac..2e769d5 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ - ~~Better UI with delta and hints for stats~~ *v0.2.0, we are here* - ~~Ability to compare player with APM-PPS-VS stats~~ - ~~Ability to fetch Tetra League leaderboard~~ -- ~~Average stats for ranks~~ *dev build are here* -- Ability to compare player with avgRank +- ~~Average stats for ranks~~ +- ~~Ability to compare player with avgRank~~ *dev build are here* - UI Animations - i18n, EN and RU locales - Talk with osk about CORS and EndContext in TL matches diff --git a/lib/gen/strings.g.dart b/lib/gen/strings.g.dart new file mode 100644 index 0000000..7a7a680 --- /dev/null +++ b/lib/gen/strings.g.dart @@ -0,0 +1,572 @@ +/// Generated file. Do not edit. +/// +/// Locales: 2 +/// Strings: 154 (77 per locale) +/// +/// Built on 2023-07-10 at 17:08 UTC + +// coverage:ignore-file +// ignore_for_file: type=lint + +import 'package:flutter/widgets.dart'; +import 'package:slang/builder/model/node.dart'; +import 'package:slang_flutter/slang_flutter.dart'; +export 'package:slang_flutter/slang_flutter.dart'; + +const AppLocale _baseLocale = AppLocale.en; + +/// Supported locales, see extension methods below. +/// +/// Usage: +/// - LocaleSettings.setLocale(AppLocale.en) // set locale +/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum +/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check +enum AppLocale with BaseAppLocale { + en(languageCode: 'en', build: _StringsEn.build), + ru(languageCode: 'ru', build: _StringsRu.build); + + const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element + + @override final String languageCode; + @override final String? scriptCode; + @override final String? countryCode; + @override final TranslationBuilder build; + + /// Gets current instance managed by [LocaleSettings]. + _StringsEn get translations => LocaleSettings.instance.translationMap[this]!; +} + +/// Method A: Simple +/// +/// No rebuild after locale change. +/// Translation happens during initialization of the widget (call of t). +/// Configurable via 'translate_var'. +/// +/// Usage: +/// String a = t.someKey.anotherKey; +/// String b = t['someKey.anotherKey']; // Only for edge cases! +_StringsEn get t => LocaleSettings.instance.currentTranslations; + +/// Method B: Advanced +/// +/// All widgets using this method will trigger a rebuild when locale changes. +/// Use this if you have e.g. a settings page where the user can select the locale during runtime. +/// +/// Step 1: +/// wrap your App with +/// TranslationProvider( +/// child: MyApp() +/// ); +/// +/// Step 2: +/// final t = Translations.of(context); // Get t variable. +/// String a = t.someKey.anotherKey; // Use t variable. +/// String b = t['someKey.anotherKey']; // Only for edge cases! +class Translations { + Translations._(); // no constructor + + static _StringsEn of(BuildContext context) => InheritedLocaleData.of(context).translations; +} + +/// The provider for method B +class TranslationProvider extends BaseTranslationProvider { + TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance); + + static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context); +} + +/// Method B shorthand via [BuildContext] extension method. +/// Configurable via 'translate_var'. +/// +/// Usage (e.g. in a widget's build method): +/// context.t.someKey.anotherKey +extension BuildContextTranslationsExtension on BuildContext { + _StringsEn get t => TranslationProvider.of(this).translations; +} + +/// Manages all translation instances and the current locale +class LocaleSettings extends BaseFlutterLocaleSettings { + LocaleSettings._() : super(utils: AppLocaleUtils.instance); + + static final instance = LocaleSettings._(); + + // static aliases (checkout base methods for documentation) + static AppLocale get currentLocale => instance.currentLocale; + static Stream getLocaleStream() => instance.getLocaleStream(); + static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale); + static AppLocale useDeviceLocale() => instance.useDeviceLocale(); + @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales; + @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw; + static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver( + language: language, + locale: locale, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ); +} + +/// Provides utility functions without any side effects. +class AppLocaleUtils extends BaseAppLocaleUtils { + AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values); + + static final instance = AppLocaleUtils._(); + + // static aliases (checkout base methods for documentation) + static AppLocale parse(String rawLocale) => instance.parse(rawLocale); + static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode); + static AppLocale findDeviceLocale() => instance.findDeviceLocale(); + static List get supportedLocales => instance.supportedLocales; + static List get supportedLocalesRaw => instance.supportedLocalesRaw; +} + +// translations + +// Path: +class _StringsEn implements BaseTranslations { + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsEn.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.en, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + dynamic operator[](String key) => $meta.getTranslation(key); + + late final _StringsEn _root = this; // ignore: unused_field + + // Translations + Map get locales => { + 'en': 'English', + 'ru': 'Russian (Русский)', + }; + String get tetraLeague => 'Tetra League'; + String get tlRecords => 'TL Records'; + String get history => 'History'; + String get sprint => '40 Lines'; + String get blitz => 'Blitz'; + String get other => 'Other'; + String get zen => 'Zen'; + String get bio => 'Bio'; + String get refresh => 'Refresh'; + String get showStoredData => 'Show stored data'; + String get statsCalc => 'Stats Calculator'; + String get settings => 'Settings'; + String get track => 'Track'; + String get stopTracking => 'Stop\ntracking'; + String get becameTracked => 'Added to tracking list!'; + String get compare => 'Compare'; + String get stoppedBeingTracked => 'Removed from tracking list!'; + String get tlLeaderboard => 'Tetra League leaderboard'; + String get noRecords => 'No records'; + String get noRecord => 'No record'; + String get notEnoughData => 'Not enough data'; + String get noHistorySaved => 'No history saved'; + String obtainDate({required Object date}) => 'Obtained ${date}'; + String fetchDate({required Object date}) => 'Fetched ${date}'; + String get exactGametime => 'Exact gametime'; + String get bigRedBanned => 'BANNED'; + String get bigRedBadStanding => 'BAD STANDING'; + String get copiedToClipboard => 'Copied to clipboard!'; + String get playerRoleAccount => ' account '; + String get wasFromBeginning => 'that was from very beginning'; + String get created => 'created'; + String get botCreatedBy => 'by'; + String get notSupporter => 'Not a supporter'; + String get assignedManualy => 'That badge was assigned manualy by TETR.IO admins'; + String supporter({required Object tier}) => 'Supporter tier ${tier}'; + String comparingWith({required Object date}) => 'Comparing with data from ${date}'; + String get top => 'Top'; + String get topRank => 'Top Rank'; + String get decaying => 'Decaying'; + String gamesUntilRanked({required Object left}) => '${left} games until being ranked'; + String get nerdStats => 'Nerd Stats'; + late final _StringsStatCellNumEn statCellNum = _StringsStatCellNumEn._(_root); + Map get playerRole => { + 'user': 'User', + 'banned': 'Banned', + 'bot': 'Bot', + 'sysop': 'System operator', + 'admin': 'Admin', + 'mod': 'Moderator', + 'halfmod': 'Community moderator', + }; + late final _StringsNumOfGameActionsEn numOfGameActions = _StringsNumOfGameActionsEn._(_root); + late final _StringsPopupActionsEn popupActions = _StringsPopupActionsEn._(_root); +} + +// Path: statCellNum +class _StringsStatCellNumEn { + _StringsStatCellNumEn._(this._root); + + final _StringsEn _root; // ignore: unused_field + + // Translations + String get xpLevel => 'XP Level'; + String get hoursPlayed => 'Hours\nPlayed'; + String get onlineGames => 'Online\nGames'; + String get gamesWon => 'Games\nWon'; + String get friends => 'Friends'; + String get apm => 'Attack\nPer Minute'; + String get vs => 'Versus\nScore'; + String get lbp => 'Leaderboard\nplacement'; + String get lbpc => 'Country LB\nplacement'; + String get gamesPlayed => 'Games\nplayed'; + String get gamesWonTL => 'Games\nWon'; + String get winrate => 'Winrate\nprecentage'; + String get level => 'Level'; + String get score => 'Score'; + String get spp => 'Score\nPer Piece'; + String get pieces => 'Pieces\nPlaced'; + String get pps => 'Pieces\nPer Second'; + String get finesseFaults => 'Finesse\nFaults'; + String get finessePercentage => 'Finesse\nPercentage'; + String get keys => 'Key\nPresses'; + String get kpp => 'KP Per\nPiece'; + String get kps => 'KP Per\nSecond'; +} + +// Path: numOfGameActions +class _StringsNumOfGameActionsEn { + _StringsNumOfGameActionsEn._(this._root); + + final _StringsEn _root; // ignore: unused_field + + // Translations + String get pc => 'All Clears'; + String get hold => 'Holds'; + String get tspinsTotal => 'T-spins total'; + String get lineClears => 'Line clears'; +} + +// Path: popupActions +class _StringsPopupActionsEn { + _StringsPopupActionsEn._(this._root); + + final _StringsEn _root; // ignore: unused_field + + // Translations + String get ok => 'OK'; +} + +// Path: +class _StringsRu implements _StringsEn { + + /// You can call this constructor and build your own translation instance of this locale. + /// Constructing via the enum [AppLocale.build] is preferred. + _StringsRu.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) + : assert(overrides == null, 'Set "translation_overrides: true" in order to enable this feature.'), + $meta = TranslationMetadata( + locale: AppLocale.ru, + overrides: overrides ?? {}, + cardinalResolver: cardinalResolver, + ordinalResolver: ordinalResolver, + ) { + $meta.setFlatMapFunction(_flatMapFunction); + } + + /// Metadata for the translations of . + @override final TranslationMetadata $meta; + + /// Access flat map + @override dynamic operator[](String key) => $meta.getTranslation(key); + + @override late final _StringsRu _root = this; // ignore: unused_field + + // Translations + @override Map get locales => { + 'en': 'Английский (English)', + 'ru': 'Русский', + }; + @override String get tetraLeague => 'Тетра Лига'; + @override String get tlRecords => 'Матчи ТЛ'; + @override String get history => 'История'; + @override String get sprint => '40 линий'; + @override String get blitz => 'Блиц'; + @override String get other => 'Другое'; + @override String get zen => 'Дзен'; + @override String get bio => 'Биография'; + @override String get refresh => 'Обновить'; + @override String get showStoredData => 'Показать сохранённые данные'; + @override String get statsCalc => 'Калькулятор статистики'; + @override String get settings => 'Настройки'; + @override String get track => 'Отслеживать'; + @override String get stopTracking => 'Перестать\nотслеживать'; + @override String get becameTracked => 'Добавлен в список отслеживания!'; + @override String get stoppedBeingTracked => 'Удалён из списка отслеживания!'; + @override String get compare => 'Сравнить'; + @override String get tlLeaderboard => 'Таблица лидеров Тетра Лиги'; + @override String get noRecords => 'Нет записей'; + @override String get noRecord => 'Нет рекорда'; + @override String get notEnoughData => 'Недостаточно данных'; + @override String get noHistorySaved => 'Нет сохранённой истории'; + @override String obtainDate({required Object date}) => 'Получено ${date}'; + @override String fetchDate({required Object date}) => 'На момент ${date}'; + @override String get exactGametime => 'Время, проведённое в игре'; + @override String get bigRedBanned => 'ЗАБАНЕН'; + @override String get bigRedBadStanding => 'ПЛОХАЯ РЕПУТАЦИЯ'; + @override String get copiedToClipboard => 'Скопировано в буфер обмена!'; + @override String get playerRoleAccount => ', аккаунт которого '; + @override String get wasFromBeginning => 'существовал с самого начала'; + @override String get created => 'создан'; + @override String get botCreatedBy => 'игроком'; + @override String get notSupporter => 'Нет саппортерки'; + @override String supporter({required Object tier}) => 'Саппортерка ${tier} уровня'; + @override String get assignedManualy => 'Этот значок был присвоен вручную администрацией TETR.IO'; + @override String comparingWith({required Object date}) => 'Сравнивая с данными от ${date}'; + @override String get top => 'Топ'; + @override String get topRank => 'Топ Ранг'; + @override String get decaying => 'Загнивает'; + @override String gamesUntilRanked({required Object left}) => '${left} матчей до получения рейтинга'; + @override String get nerdStats => 'Для задротов'; + @override late final _StringsStatCellNumRu statCellNum = _StringsStatCellNumRu._(_root); + @override Map get playerRole => { + 'user': 'Пользователь', + 'banned': 'Заблокированный пользователь', + 'bot': 'Бот', + 'sysop': 'Системный оператор', + 'admin': 'Администратор', + 'mod': 'Модератор', + 'halfmod': 'Модератор сообщества', + }; + @override late final _StringsNumOfGameActionsRu numOfGameActions = _StringsNumOfGameActionsRu._(_root); + @override late final _StringsPopupActionsRu popupActions = _StringsPopupActionsRu._(_root); +} + +// Path: statCellNum +class _StringsStatCellNumRu implements _StringsStatCellNumEn { + _StringsStatCellNumRu._(this._root); + + @override final _StringsRu _root; // ignore: unused_field + + // Translations + @override String get xpLevel => 'Уровень\nопыта'; + @override String get hoursPlayed => 'Часов\nСыграно'; + @override String get onlineGames => 'Онлайн\nИгр'; + @override String get gamesWon => 'Онлайн\nПобед'; + @override String get friends => 'Друзей'; + @override String get apm => 'Атака в\nМинуту'; + @override String get vs => 'Показатель\nVersus'; + @override String get lbp => 'Положение\nв рейтинге'; + @override String get lbpc => 'Положение\nв рейтинге страны'; + @override String get gamesPlayed => 'Игр\nСыграно'; + @override String get gamesWonTL => 'Побед'; + @override String get winrate => 'Процент\nпобед'; + @override String get level => 'Уровень'; + @override String get score => 'Счёт'; + @override String get spp => 'Очков\nна Фигуру'; + @override String get pieces => 'Фигур\nУстановлено'; + @override String get pps => 'Фигур в\nСекунду'; + @override String get finesseFaults => 'Ошибок\nТехники'; + @override String get finessePercentage => '% Качества\nТехники'; + @override String get keys => 'Нажатий\nКлавиш'; + @override String get kpp => 'Нажатий\nна Фигуру'; + @override String get kps => 'Нажатий\nв Секунду'; +} + +// Path: numOfGameActions +class _StringsNumOfGameActionsRu implements _StringsNumOfGameActionsEn { + _StringsNumOfGameActionsRu._(this._root); + + @override final _StringsRu _root; // ignore: unused_field + + // Translations + @override String get pc => 'Все чисто'; + @override String get hold => 'В запас'; + @override String get tspinsTotal => 'T-spins всего'; + @override String get lineClears => 'Линий очищено'; +} + +// Path: popupActions +class _StringsPopupActionsRu implements _StringsPopupActionsEn { + _StringsPopupActionsRu._(this._root); + + @override final _StringsRu _root; // ignore: unused_field + + // Translations + @override String get ok => 'OK'; +} + +/// Flat map(s) containing all translations. +/// Only for edge cases! For simple maps, use the map function of this library. + +extension on _StringsEn { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'locales.en': return 'English'; + case 'locales.ru': return 'Russian (Русский)'; + case 'tetraLeague': return 'Tetra League'; + case 'tlRecords': return 'TL Records'; + case 'history': return 'History'; + case 'sprint': return '40 Lines'; + case 'blitz': return 'Blitz'; + case 'other': return 'Other'; + case 'zen': return 'Zen'; + case 'bio': return 'Bio'; + case 'refresh': return 'Refresh'; + case 'showStoredData': return 'Show stored data'; + case 'statsCalc': return 'Stats Calculator'; + case 'settings': return 'Settings'; + case 'track': return 'Track'; + case 'stopTracking': return 'Stop\ntracking'; + case 'becameTracked': return 'Added to tracking list!'; + case 'compare': return 'Compare'; + case 'stoppedBeingTracked': return 'Removed from tracking list!'; + case 'tlLeaderboard': return 'Tetra League leaderboard'; + case 'noRecords': return 'No records'; + case 'noRecord': return 'No record'; + case 'notEnoughData': return 'Not enough data'; + case 'noHistorySaved': return 'No history saved'; + case 'obtainDate': return ({required Object date}) => 'Obtained ${date}'; + case 'fetchDate': return ({required Object date}) => 'Fetched ${date}'; + case 'exactGametime': return 'Exact gametime'; + case 'bigRedBanned': return 'BANNED'; + case 'bigRedBadStanding': return 'BAD STANDING'; + case 'copiedToClipboard': return 'Copied to clipboard!'; + case 'playerRoleAccount': return ' account '; + case 'wasFromBeginning': return 'that was from very beginning'; + case 'created': return 'created'; + case 'botCreatedBy': return 'by'; + case 'notSupporter': return 'Not a supporter'; + case 'assignedManualy': return 'That badge was assigned manualy by TETR.IO admins'; + case 'supporter': return ({required Object tier}) => 'Supporter tier ${tier}'; + case 'comparingWith': return ({required Object date}) => 'Comparing with data from ${date}'; + case 'top': return 'Top'; + case 'topRank': return 'Top Rank'; + case 'decaying': return 'Decaying'; + case 'gamesUntilRanked': return ({required Object left}) => '${left} games until being ranked'; + case 'nerdStats': return 'Nerd Stats'; + case 'statCellNum.xpLevel': return 'XP Level'; + case 'statCellNum.hoursPlayed': return 'Hours\nPlayed'; + case 'statCellNum.onlineGames': return 'Online\nGames'; + case 'statCellNum.gamesWon': return 'Games\nWon'; + case 'statCellNum.friends': return 'Friends'; + case 'statCellNum.apm': return 'Attack\nPer Minute'; + case 'statCellNum.vs': return 'Versus\nScore'; + case 'statCellNum.lbp': return 'Leaderboard\nplacement'; + case 'statCellNum.lbpc': return 'Country LB\nplacement'; + case 'statCellNum.gamesPlayed': return 'Games\nplayed'; + case 'statCellNum.gamesWonTL': return 'Games\nWon'; + case 'statCellNum.winrate': return 'Winrate\nprecentage'; + case 'statCellNum.level': return 'Level'; + case 'statCellNum.score': return 'Score'; + case 'statCellNum.spp': return 'Score\nPer Piece'; + case 'statCellNum.pieces': return 'Pieces\nPlaced'; + case 'statCellNum.pps': return 'Pieces\nPer Second'; + case 'statCellNum.finesseFaults': return 'Finesse\nFaults'; + case 'statCellNum.finessePercentage': return 'Finesse\nPercentage'; + case 'statCellNum.keys': return 'Key\nPresses'; + case 'statCellNum.kpp': return 'KP Per\nPiece'; + case 'statCellNum.kps': return 'KP Per\nSecond'; + case 'playerRole.user': return 'User'; + case 'playerRole.banned': return 'Banned'; + case 'playerRole.bot': return 'Bot'; + case 'playerRole.sysop': return 'System operator'; + case 'playerRole.admin': return 'Admin'; + case 'playerRole.mod': return 'Moderator'; + case 'playerRole.halfmod': return 'Community moderator'; + case 'numOfGameActions.pc': return 'All Clears'; + case 'numOfGameActions.hold': return 'Holds'; + case 'numOfGameActions.tspinsTotal': return 'T-spins total'; + case 'numOfGameActions.lineClears': return 'Line clears'; + case 'popupActions.ok': return 'OK'; + default: return null; + } + } +} + +extension on _StringsRu { + dynamic _flatMapFunction(String path) { + switch (path) { + case 'locales.en': return 'Английский (English)'; + case 'locales.ru': return 'Русский'; + case 'tetraLeague': return 'Тетра Лига'; + case 'tlRecords': return 'Матчи ТЛ'; + case 'history': return 'История'; + case 'sprint': return '40 линий'; + case 'blitz': return 'Блиц'; + case 'other': return 'Другое'; + case 'zen': return 'Дзен'; + case 'bio': return 'Биография'; + case 'refresh': return 'Обновить'; + case 'showStoredData': return 'Показать сохранённые данные'; + case 'statsCalc': return 'Калькулятор статистики'; + case 'settings': return 'Настройки'; + case 'track': return 'Отслеживать'; + case 'stopTracking': return 'Перестать\nотслеживать'; + case 'becameTracked': return 'Добавлен в список отслеживания!'; + case 'stoppedBeingTracked': return 'Удалён из списка отслеживания!'; + case 'compare': return 'Сравнить'; + case 'tlLeaderboard': return 'Таблица лидеров Тетра Лиги'; + case 'noRecords': return 'Нет записей'; + case 'noRecord': return 'Нет рекорда'; + case 'notEnoughData': return 'Недостаточно данных'; + case 'noHistorySaved': return 'Нет сохранённой истории'; + case 'obtainDate': return ({required Object date}) => 'Получено ${date}'; + case 'fetchDate': return ({required Object date}) => 'На момент ${date}'; + case 'exactGametime': return 'Время, проведённое в игре'; + case 'bigRedBanned': return 'ЗАБАНЕН'; + case 'bigRedBadStanding': return 'ПЛОХАЯ РЕПУТАЦИЯ'; + case 'copiedToClipboard': return 'Скопировано в буфер обмена!'; + case 'playerRoleAccount': return ', аккаунт которого '; + case 'wasFromBeginning': return 'существовал с самого начала'; + case 'created': return 'создан'; + case 'botCreatedBy': return 'игроком'; + case 'notSupporter': return 'Нет саппортерки'; + case 'supporter': return ({required Object tier}) => 'Саппортерка ${tier} уровня'; + case 'assignedManualy': return 'Этот значок был присвоен вручную администрацией TETR.IO'; + case 'comparingWith': return ({required Object date}) => 'Сравнивая с данными от ${date}'; + case 'top': return 'Топ'; + case 'topRank': return 'Топ Ранг'; + case 'decaying': return 'Загнивает'; + case 'gamesUntilRanked': return ({required Object left}) => '${left} матчей до получения рейтинга'; + case 'nerdStats': return 'Для задротов'; + case 'statCellNum.xpLevel': return 'Уровень\nопыта'; + case 'statCellNum.hoursPlayed': return 'Часов\nСыграно'; + case 'statCellNum.onlineGames': return 'Онлайн\nИгр'; + case 'statCellNum.gamesWon': return 'Онлайн\nПобед'; + case 'statCellNum.friends': return 'Друзей'; + case 'statCellNum.apm': return 'Атака в\nМинуту'; + case 'statCellNum.vs': return 'Показатель\nVersus'; + case 'statCellNum.lbp': return 'Положение\nв рейтинге'; + case 'statCellNum.lbpc': return 'Положение\nв рейтинге страны'; + case 'statCellNum.gamesPlayed': return 'Игр\nСыграно'; + case 'statCellNum.gamesWonTL': return 'Побед'; + case 'statCellNum.winrate': return 'Процент\nпобед'; + case 'statCellNum.level': return 'Уровень'; + case 'statCellNum.score': return 'Счёт'; + case 'statCellNum.spp': return 'Очков\nна Фигуру'; + case 'statCellNum.pieces': return 'Фигур\nУстановлено'; + case 'statCellNum.pps': return 'Фигур в\nСекунду'; + case 'statCellNum.finesseFaults': return 'Ошибок\nТехники'; + case 'statCellNum.finessePercentage': return '% Качества\nТехники'; + case 'statCellNum.keys': return 'Нажатий\nКлавиш'; + case 'statCellNum.kpp': return 'Нажатий\nна Фигуру'; + case 'statCellNum.kps': return 'Нажатий\nв Секунду'; + case 'playerRole.user': return 'Пользователь'; + case 'playerRole.banned': return 'Заблокированный пользователь'; + case 'playerRole.bot': return 'Бот'; + case 'playerRole.sysop': return 'Системный оператор'; + case 'playerRole.admin': return 'Администратор'; + case 'playerRole.mod': return 'Модератор'; + case 'playerRole.halfmod': return 'Модератор сообщества'; + case 'numOfGameActions.pc': return 'Все чисто'; + case 'numOfGameActions.hold': return 'В запас'; + case 'numOfGameActions.tspinsTotal': return 'T-spins всего'; + case 'numOfGameActions.lineClears': return 'Линий очищено'; + case 'popupActions.ok': return 'OK'; + default: return null; + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 086d2a7..71679f0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:tetra_stats/views/main_view.dart'; import 'package:tetra_stats/views/settings_view.dart'; import 'package:tetra_stats/views/tracked_players_view.dart'; @@ -11,11 +13,26 @@ void main() { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; } - runApp(MaterialApp( - home: const MainView(), - routes: {"/settings": (context) => const SettingsView(), "/states": (context) => const TrackedPlayersView(), "/calc": (context) => const CalcView()}, - theme: ThemeData( - fontFamily: 'Eurostile Round', - colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), - scaffoldBackgroundColor: Colors.black))); + WidgetsFlutterBinding.ensureInitialized(); + runApp(TranslationProvider( + child: MyApp(), + )); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: const MainView(), + locale: TranslationProvider.of(context).flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + routes: {"/settings": (context) => const SettingsView(), "/states": (context) => const TrackedPlayersView(), "/calc": (context) => const CalcView()}, + theme: ThemeData( + fontFamily: 'Eurostile Round', + colorScheme: const ColorScheme.dark(primary: Colors.cyanAccent, secondary: Colors.white), + scaffoldBackgroundColor: Colors.black + ) + ); + } } diff --git a/lib/views/compare_view.dart b/lib/views/compare_view.dart index 30eb882..d97a01e 100644 --- a/lib/views/compare_view.dart +++ b/lib/views/compare_view.dart @@ -55,6 +55,18 @@ class CompareState extends State { void fetchRedSide(String user) async { try { + if (user.startsWith("\$avg")){ + try{ + var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; + redSideMode = Mode.averages; + theRedSide = [null, null, average]; + return setState(() {}); + }on Exception { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Falied to assign $user"))); + return; + } + } var tearDownToNumbers = numbersReg.allMatches(user); if (tearDownToNumbers.length == 3) { redSideMode = Mode.stats; @@ -118,6 +130,18 @@ class CompareState extends State { void fetchGreenSide(String user) async { try { + if (user.startsWith("\$avg")){ + try{ + var average = (await teto.fetchTLLeaderboard()).getAverageOfRank(user.substring(4).toLowerCase())[0]; + greenSideMode = Mode.averages; + theGreenSide = [null, null, average]; + return setState(() {}); + }on Exception { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text("Falied to assign $user"))); + return; + } + } var tearDownToNumbers = numbersReg.allMatches(user); if (tearDownToNumbers.length == 3) { greenSideMode = Mode.stats; @@ -211,7 +235,7 @@ class CompareState extends State { titleGreenSide = "${theGreenSide[2].apm} APM, ${theGreenSide[2].pps} PPS, ${theGreenSide[2].vs} VS"; break; case Mode.averages: - titleGreenSide = "average"; + titleGreenSide = "Average ${theGreenSide[2].rank.toUpperCase()} rank"; break; } switch (redSideMode){ @@ -222,7 +246,7 @@ class CompareState extends State { titleRedSide = "${theRedSide[2].apm} APM, ${theRedSide[2].pps} PPS, ${theRedSide[2].vs} VS"; break; case Mode.averages: - titleRedSide = "average"; + titleRedSide = "Average ${theRedSide[2].rank.toUpperCase()} rank"; break; } return Scaffold( @@ -376,8 +400,8 @@ class CompareState extends State { ), if (theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9 && - greenSideMode == Mode.player && - redSideMode == Mode.player) + greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "TR", greenSide: theGreenSide[2].rating, @@ -385,24 +409,24 @@ class CompareState extends State { fractionDigits: 2, higherIsBetter: true, ), - if (greenSideMode == Mode.player && - redSideMode == Mode.player) + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "Games Played", greenSide: theGreenSide[2].gamesPlayed, redSide: theRedSide[2].gamesPlayed, higherIsBetter: true, ), - if (greenSideMode == Mode.player && - redSideMode == Mode.player) + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "Games Won", greenSide: theGreenSide[2].gamesWon, redSide: theRedSide[2].gamesWon, higherIsBetter: true, ), - if (greenSideMode == Mode.player && - redSideMode == Mode.player) + if (greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "WR %", greenSide: @@ -413,8 +437,8 @@ class CompareState extends State { ), if (theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9 && - greenSideMode == Mode.player && - redSideMode == Mode.player) + greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "Glicko", greenSide: theGreenSide[2].glicko!, @@ -424,8 +448,8 @@ class CompareState extends State { ), if (theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9 && - greenSideMode == Mode.player && - redSideMode == Mode.player) + greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "RD", greenSide: theGreenSide[2].rd!, @@ -575,8 +599,8 @@ class CompareState extends State { ), if (theGreenSide[2].gamesPlayed > 9 && theGreenSide[2].gamesPlayed > 9 && - greenSideMode == Mode.player && - redSideMode == Mode.player) + greenSideMode != Mode.stats && + redSideMode != Mode.stats) CompareThingy( label: "Acc. of Est.", greenSide: theGreenSide[2].esttracc!, @@ -781,7 +805,8 @@ class CompareState extends State { fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), ), - if (greenSideMode == Mode.player && redSideMode == Mode.player) + if (greenSideMode != Mode.stats && redSideMode != Mode.stats && + theGreenSide[2].gamesPlayed > 9 && theRedSide[2].gamesPlayed > 9) CompareThingy( label: "By Glicko", greenSide: getWinrateByTR( @@ -803,15 +828,15 @@ class CompareState extends State { label: "By Est. TR", greenSide: getWinrateByTR( theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd!, + theGreenSide[2].rd ?? noTrRd, theRedSide[2].estTr!.estglicko, - theRedSide[2].rd!) * + theRedSide[2].rd ?? noTrRd) * 100, redSide: getWinrateByTR( theRedSide[2].estTr!.estglicko, - theRedSide[2].rd!, + theRedSide[2].rd ?? noTrRd, theGreenSide[2].estTr!.estglicko, - theGreenSide[2].rd!) * + theGreenSide[2].rd ?? noTrRd) * 100, fractionDigits: 2, higherIsBetter: true, @@ -834,7 +859,7 @@ class PlayerSelector extends StatelessWidget { final Function fetch; final Function change; final Function updateState; - const PlayerSelector( + PlayerSelector( {super.key, required this.data, required this.mode, @@ -845,16 +870,30 @@ class PlayerSelector extends StatelessWidget { @override Widget build(BuildContext context) { final TextEditingController playerController = TextEditingController(); + String underFieldString = ""; if (!listEquals(data, [null, null, null])){ switch (mode){ case Mode.player: - playerController.text = data[0] != null ? data[0].username : "???"; + playerController.text = data[0] != null ? data[0].username : ""; break; case Mode.stats: playerController.text = "${data[2].apm} ${data[2].pps} ${data[2].vs}"; break; case Mode.averages: - playerController.text = "average"; + playerController.text = "\$avg${data[2].rank.toUpperCase()}"; + break; + } + } + if (!listEquals(data, [null, null, null])){ + switch (mode){ + case Mode.player: + underFieldString = data[0] != null ? data[0].toString() : "???"; + break; + case Mode.stats: + underFieldString = "${data[2].apm} APM, ${data[2].pps} PPS, ${data[2].vs} VS"; + break; + case Mode.averages: + underFieldString = "Average ${data[2].rank.toUpperCase()} rank"; break; } } @@ -867,11 +906,20 @@ class PlayerSelector extends StatelessWidget { controller: playerController, decoration: const InputDecoration(counter: Offstage()), onSubmitted: (String value) { + underFieldString = "Fetching..."; fetch(value); }), - if (data[0] != null && data[1] == null) - Text( - data[0].toString(), + if (data[0] != null && data[1] != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: DropdownButton( + items: data[1], + value: data[0], + onChanged: (value) => change(value!), + ), + ) + else Text( + underFieldString, style: const TextStyle( shadows: [ Shadow( @@ -887,15 +935,6 @@ class PlayerSelector extends StatelessWidget { ], ), ), - if (data[0] != null && data[1] != null) - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: DropdownButton( - items: data[1], - value: data[0], - onChanged: (value) => change(value!), - ), - ) ], ); } diff --git a/lib/views/main_view.dart b/lib/views/main_view.dart index 88d32e0..d4273bb 100644 --- a/lib/views/main_view.dart +++ b/lib/views/main_view.dart @@ -6,6 +6,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/services.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/views/tl_leaderboard_view.dart' show TLLeaderboardView; @@ -28,7 +29,7 @@ const givenTextHeightByScreenPercentage = 0.3; final NumberFormat timeInSec = NumberFormat("#,###.###s."); final NumberFormat f2 = NumberFormat.decimalPatternDigits(decimalDigits: 2); final NumberFormat f4 = NumberFormat.decimalPatternDigits(decimalDigits: 4); -final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); +final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale).add_Hms(); class MainView extends StatefulWidget { final String? player; @@ -46,14 +47,6 @@ Future copyToClipboard(String text) async { class _MainState extends State with SingleTickerProviderStateMixin { final bodyGlobalKey = GlobalKey(); - final List myTabs = [ - const Tab(text: "Tetra League"), - const Tab(text: "TL Records"), - const Tab(text: "History"), - const Tab(text: "40 Lines"), - const Tab(text: "Blitz"), - const Tab(text: "Other"), - ]; bool _searchBoolean = false; late TabController _tabController; late ScrollController _scrollController; @@ -182,6 +175,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { + final t = Translations.of(context); return Scaffold( drawer: widget.player == null ? NavDrawer(changePlayer) : null, appBar: AppBar( @@ -227,21 +221,21 @@ class _MainState extends State with SingleTickerProviderStateMixin { ), PopupMenuButton( itemBuilder: (BuildContext context) => [ - const PopupMenuItem( + PopupMenuItem( value: "refresh", - child: Text('Refresh'), + child: Text(t.refresh), ), - const PopupMenuItem( + PopupMenuItem( value: "/states", - child: Text('Show stored data'), + child: Text(t.showStoredData), ), - const PopupMenuItem( + PopupMenuItem( value: "/calc", - child: Text('Stats Calculator'), + child: Text(t.statsCalc), ), - const PopupMenuItem( + PopupMenuItem( value: "/settings", - child: Text('Settings'), + child: Text(t.settings), ), ], onSelected: (value) { @@ -258,22 +252,9 @@ class _MainState extends State with SingleTickerProviderStateMixin { builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: - return const Center( - child: Text('none case of FutureBuilder', - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42), - textAlign: TextAlign.center)); case ConnectionState.waiting: - return const Center( - child: CircularProgressIndicator(color: Colors.white)); case ConnectionState.active: - return const Center( - child: Text('active case of FutureBuilder', - style: TextStyle( - fontFamily: "Eurostile Round Extended", - fontSize: 42), - textAlign: TextAlign.center)); + return const Center(child: CircularProgressIndicator(color: Colors.white)); case ConnectionState.done: //bool bigScreen = MediaQuery.of(context).size.width > 1024; if (snapshot.hasData) { @@ -303,7 +284,14 @@ class _MainState extends State with SingleTickerProviderStateMixin { child: TabBar( controller: _tabController, isScrollable: true, - tabs: myTabs, + tabs: [ + Tab(text: t.tetraLeague), + Tab(text: t.tlRecords), + Tab(text: t.history), + Tab(text: t.sprint), + Tab(text: t.blitz), + Tab(text: t.other), + ], ), ), ]; @@ -314,7 +302,7 @@ class _MainState extends State with SingleTickerProviderStateMixin { TLThingy( tl: snapshot.data![0].tlSeason1, userID: snapshot.data![0].userId, - oldTl: snapshot.data![4],), + oldTl: snapshot.data![4]), _TLRecords(userID: snapshot.data![0].userId, data: snapshot.data![3]), _History(states: snapshot.data![2], update: _justUpdate), _RecordThingy( @@ -406,7 +394,6 @@ class _NavDrawerState extends State { builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.none: - return const Center(child: Text('none case of StreamBuilder')); case ConnectionState.waiting: case ConnectionState.active: final allPlayers = (snapshot.data != null) @@ -436,7 +423,7 @@ class _NavDrawerState extends State { SliverToBoxAdapter( child: ListTile( leading: const Icon(Icons.leaderboard), - title: const Text("Tetra League leaderboard"), + title: Text(t.tlLeaderboard), onTap: () { Navigator.push( context, @@ -501,7 +488,7 @@ class _TLRecords extends StatelessWidget { ), );}, )] - : [const Center(child: Text("No records", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], + : [Center(child: Text(t.noRecords, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))], ); } } @@ -527,10 +514,10 @@ class _History extends StatelessWidget{ } ), if(chartsData[chartsIndex].value!.length > 1) _HistoryChartThigy(data: chartsData[chartsIndex].value!, title: "ss", yAxisTitle: chartsShortTitles[chartsIndex], bigScreen: bigScreen, leftSpace: bigScreen? 80 : 45, yFormat: bigScreen? f2 : NumberFormat.compact(),) - else const Center(child: Text("Not enough data", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) + else Center(child: Text(t.notEnoughData, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28))) ], ), - ] : [const Center(child: Text("No history saved", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); + ] : [Center(child: Text(t.noHistorySaved, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)))]); } } @@ -599,12 +586,12 @@ class _RecordThingy extends StatelessWidget { children: (record != null) ? [ if (record!.stream.contains("40l")) - Text("40 Lines", + Text(t.sprint, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)) else if (record!.stream.contains("blitz")) - Text("Blitz", + Text(t.blitz, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -629,7 +616,7 @@ class _RecordThingy extends StatelessWidget { playerStatLabel: "Leaderboard Placement", isScreenBig: bigScreen, higherIsBetter: false), - Text("Obtained ${dateFormat.format(record!.timestamp!)}", + Text(t.obtainDate(date: dateFormat.format(record!.timestamp!)), textAlign: TextAlign.center, style: const TextStyle( fontFamily: "Eurostile Round", @@ -647,53 +634,53 @@ class _RecordThingy extends StatelessWidget { if (record!.stream.contains("blitz")) StatCellNum( playerStat: record!.endContext!.level, - playerStatLabel: "Level", + playerStatLabel: t.statCellNum.level, isScreenBig: bigScreen, higherIsBetter: true,), if (record!.stream.contains("blitz")) StatCellNum( playerStat: record!.endContext!.spp, - playerStatLabel: "Score\nPer Piece", + playerStatLabel: t.statCellNum.spp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true,), StatCellNum( playerStat: record!.endContext!.piecesPlaced, - playerStatLabel: "Pieces\nPlaced", + playerStatLabel: t.statCellNum.pieces, isScreenBig: bigScreen, higherIsBetter: true,), StatCellNum( playerStat: record!.endContext!.pps, - playerStatLabel: "Pieces\nPer Second", + playerStatLabel: t.statCellNum.pps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true,), StatCellNum( playerStat: record!.endContext!.finesse.faults, - playerStatLabel: "Finesse\nFaults", + playerStatLabel: t.statCellNum.finesseFaults, isScreenBig: bigScreen, higherIsBetter: false,), StatCellNum( playerStat: record!.endContext!.finessePercentage * 100, - playerStatLabel: "Finesse\nPercentage", + playerStatLabel: t.statCellNum.finessePercentage, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true,), StatCellNum( playerStat: record!.endContext!.inputs, - playerStatLabel: "Key\nPresses", + playerStatLabel: t.statCellNum.keys, isScreenBig: bigScreen, higherIsBetter: false,), StatCellNum( playerStat: record!.endContext!.kpp, - playerStatLabel: "KP Per\nPiece", + playerStatLabel: t.statCellNum.kpp, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: false,), StatCellNum( playerStat: record!.endContext!.kps, - playerStatLabel: "KP Per\nSecond", + playerStatLabel: t.statCellNum.kps, fractionDigits: 2, isScreenBig: bigScreen, higherIsBetter: true,), @@ -713,8 +700,8 @@ class _RecordThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("All Clears:", - style: TextStyle(fontSize: 24)), + Text("${t.numOfGameActions.pc}:", + style: const TextStyle(fontSize: 24)), Text( record!.endContext!.clears.allClears .toString(), @@ -726,8 +713,8 @@ class _RecordThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("Holds:", - style: TextStyle(fontSize: 24)), + Text("${t.numOfGameActions.hold}:", + style: const TextStyle(fontSize: 24)), Text( record!.endContext!.holds.toString(), style: const TextStyle(fontSize: 24), @@ -738,8 +725,8 @@ class _RecordThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("T-spins total:", - style: TextStyle(fontSize: 24)), + Text("${t.numOfGameActions.tspinsTotal}:", + style: const TextStyle(fontSize: 24)), Text( record!.endContext!.tSpins.toString(), style: const TextStyle(fontSize: 24), @@ -841,8 +828,8 @@ class _RecordThingy extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text("Line clears:", - style: TextStyle(fontSize: 24)), + Text("${t.numOfGameActions.lineClears}:", + style: const TextStyle(fontSize: 24)), Text( record!.endContext!.lines.toString(), style: const TextStyle(fontSize: 24), @@ -906,7 +893,7 @@ class _RecordThingy extends StatelessWidget { ), ] : [ - const Text("No record", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)) + Text(t.noRecord, style: const TextStyle(fontFamily: "Eurostile Round Extended", fontSize: 28)) ], ); }); @@ -930,7 +917,7 @@ class _OtherThingy extends StatelessWidget { itemBuilder: (BuildContext context, int index) { return Column( children: [ - Text("Other info", + Text(t.other, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), @@ -939,17 +926,17 @@ class _OtherThingy extends StatelessWidget { padding: const EdgeInsets.fromLTRB(0, 48, 0, 48), child: Column( children: [ - Text("Zen", + Text(t.zen, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text( - "Level ${NumberFormat.decimalPattern().format(zen!.level)}", + "${t.statCellNum.level} ${NumberFormat.decimalPattern().format(zen!.level)}", style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Text( - "Score ${NumberFormat.decimalPattern().format(zen!.score)}", + "${t.statCellNum.score} ${NumberFormat.decimalPattern().format(zen!.score)}", style: const TextStyle(fontSize: 18)), ], ), @@ -959,7 +946,7 @@ class _OtherThingy extends StatelessWidget { padding: const EdgeInsets.fromLTRB(0, 0, 0, 48), child: Column( children: [ - Text("Bio", + Text(t.bio, style: TextStyle( fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index 9f3b1ab..1a22c3c 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/services/crud_exceptions.dart'; import 'package:tetra_stats/services/tetrio_crud.dart'; @@ -65,6 +66,12 @@ class SettingsState extends State { @override Widget build(BuildContext context) { + final t = Translations.of(context); + List>? locales = >[]; + for (var v in AppLocale.values){ + locales.add(DropdownMenuItem( + value: v, child: Text(t.locales[v.languageTag]!))); + } return Scaffold( appBar: AppBar( title: const Text("Settings"), @@ -212,6 +219,14 @@ class SettingsState extends State { ], )), ), + ListTile( + title: const Text("Language"), + trailing: DropdownButton( + items: locales, + value: LocaleSettings.currentLocale, + onChanged: (value) => LocaleSettings.setLocale(value!), + ), + ), const Divider(), ListTile( title: const Text("About app"), diff --git a/lib/widgets/tl_thingy.dart b/lib/widgets/tl_thingy.dart index d0df86c..9e8aaa4 100644 --- a/lib/widgets/tl_thingy.dart +++ b/lib/widgets/tl_thingy.dart @@ -3,6 +3,8 @@ import 'package:intl/intl.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:syncfusion_flutter_gauges/gauges.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; +import 'package:tetra_stats/views/calc_view.dart'; import 'package:tetra_stats/widgets/stat_sell_num.dart'; var fDiff = NumberFormat("+#,###.###;-#,###.###"); @@ -17,6 +19,8 @@ class TLThingy extends StatelessWidget { @override Widget build(BuildContext context) { + final t = Translations.of(context); + final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; return ListView.builder( @@ -26,8 +30,8 @@ class TLThingy extends StatelessWidget { return Column( children: (tl.gamesPlayed > 0) ? [ - Text("Tetra League", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), - if (oldTl != null) Text("Comparing with data from ${DateFormat.yMMMd().add_Hms().format(oldTl!.timestamp)}"), + Text(t.tetraLeague, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + if (oldTl != null) Text(t.comparingWith(date: dateFormat.format(oldTl!.timestamp))), if (tl.gamesPlayed >= 10) Wrap( direction: Axis.horizontal, @@ -51,7 +55,7 @@ class TLThingy extends StatelessWidget { ), ), Text( - "Top ${f2.format(tl.percentile * 100)}% (${tl.percentileRank.toUpperCase()}) • Top Rank: ${tl.bestRank.toUpperCase()} • Glicko: ${f2.format(tl.glicko!)}±${f2.format(tl.rd!)}${tl.decaying ? ' • Decaying' : ''}", + "${t.top} ${f2.format(tl.percentile * 100)}% (${tl.percentileRank.toUpperCase()}) • ${t.topRank}: ${tl.bestRank.toUpperCase()} • Glicko: ${f2.format(tl.glicko!)}±${f2.format(tl.rd!)}${tl.decaying ? ' • ${t.decaying}' : ''}", textAlign: TextAlign.center, ), ], @@ -73,7 +77,7 @@ class TLThingy extends StatelessWidget { ), ), if (tl.gamesPlayed < 10) - Text("${10 - tl.gamesPlayed} games until being ranked", + Text(t.gamesUntilRanked(left: 10 - tl.gamesPlayed), softWrap: true, textAlign: TextAlign.center, style: TextStyle( @@ -90,21 +94,21 @@ class TLThingy extends StatelessWidget { crossAxisAlignment: WrapCrossAlignment.start, clipBehavior: Clip.hardEdge, children: [ - if (tl.apm != null) StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Attack\nPer Minute", higherIsBetter: true, oldPlayerStat: oldTl?.apm), - if (tl.pps != null) StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Pieces\nPer Second", higherIsBetter: true, oldPlayerStat: oldTl?.pps), - if (tl.vs != null) StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Versus\nScore", higherIsBetter: true, oldPlayerStat: oldTl?.vs), - if (tl.standing > 0) StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: "Leaderboard\nplacement", higherIsBetter: false, oldPlayerStat: oldTl?.standing), - if (tl.standingLocal > 0) StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: "Country LB\nplacement", higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), - StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: "Games\nplayed", higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed), - StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: "Games\nwon", higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon), - StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: "Winrate\nprecentage", higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null), + if (tl.apm != null) StatCellNum(playerStat: tl.apm!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.apm, higherIsBetter: true, oldPlayerStat: oldTl?.apm), + if (tl.pps != null) StatCellNum(playerStat: tl.pps!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.pps, higherIsBetter: true, oldPlayerStat: oldTl?.pps), + if (tl.vs != null) StatCellNum(playerStat: tl.vs!, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.vs, higherIsBetter: true, oldPlayerStat: oldTl?.vs), + if (tl.standing > 0) StatCellNum(playerStat: tl.standing, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbp, higherIsBetter: false, oldPlayerStat: oldTl?.standing), + if (tl.standingLocal > 0) StatCellNum(playerStat: tl.standingLocal, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.lbpc, higherIsBetter: false, oldPlayerStat: oldTl?.standingLocal), + StatCellNum(playerStat: tl.gamesPlayed, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesPlayed, higherIsBetter: true, oldPlayerStat: oldTl?.gamesPlayed), + StatCellNum(playerStat: tl.gamesWon, isScreenBig: bigScreen, playerStatLabel: t.statCellNum.gamesWonTL, higherIsBetter: true, oldPlayerStat: oldTl?.gamesWon), + StatCellNum(playerStat: tl.winrate * 100, isScreenBig: bigScreen, fractionDigits: 2, playerStatLabel: t.statCellNum.winrate, higherIsBetter: true, oldPlayerStat: oldTl != null ? oldTl!.winrate*100 : null), ], ), ), if (tl.nerdStats != null) Column( children: [ - Text("Nerd Stats", style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), + Text(t.nerdStats, style: TextStyle(fontFamily: "Eurostile Round Extended", fontSize: bigScreen ? 42 : 28)), Padding( padding: const EdgeInsets.fromLTRB(0, 40, 0, 0), child: Wrap( diff --git a/lib/widgets/user_thingy.dart b/lib/widgets/user_thingy.dart index 95227ef..4a15259 100644 --- a/lib/widgets/user_thingy.dart +++ b/lib/widgets/user_thingy.dart @@ -1,23 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:tetra_stats/data_objects/tetrio.dart'; +import 'package:tetra_stats/gen/strings.g.dart'; import 'package:tetra_stats/views/compare_view.dart'; import 'package:intl/intl.dart'; import 'dart:developer' as developer; import 'package:tetra_stats/widgets/stat_sell_num.dart'; -extension StringExtension on String { - String capitalize() { - return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; - } -} - Future copyToClipboard(String text) async { await Clipboard.setData(ClipboardData(text: text)); } -final DateFormat dateFormat = DateFormat.yMMMd().add_Hms(); - class UserThingy extends StatelessWidget { final TetrioPlayer player; final bool showStateTimestamp; @@ -26,6 +19,8 @@ class UserThingy extends StatelessWidget { @override Widget build(BuildContext context) { + final t = Translations.of(context); + final DateFormat dateFormat = DateFormat.yMMMd(LocaleSettings.currentLocale.languageCode).add_Hms(); return LayoutBuilder(builder: (context, constraints) { bool bigScreen = constraints.maxWidth > 768; double bannerHeight = bigScreen ? 240 : 120; @@ -99,12 +94,12 @@ class UserThingy extends StatelessWidget { child: Text(player.userId, style: const TextStyle(fontFamily: "Eurostile Round Condensed", fontSize: 14)), onPressed: () { copyToClipboard(player.userId); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Copied to clipboard!"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.copiedToClipboard))); }), ], )), showStateTimestamp - ? Text("Fetched ${dateFormat.format(player.state)}") + ? Text(t.fetchDate(date: dateFormat.format(player.state))) : Wrap(direction: Axis.horizontal, alignment: WrapAlignment.center, spacing: 25, crossAxisAlignment: WrapCrossAlignment.start, children: [ FutureBuilder( future: teto.isPlayerTracking(player.userId), @@ -121,10 +116,10 @@ class UserThingy extends StatelessWidget { icon: const Icon(Icons.person_remove), onPressed: () { teto.deletePlayerToTrack(player.userId).then((value) => setState()); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Removed from tracking list!"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.stoppedBeingTracked))); }, ), - const Text("Stop tracking") + Text(t.stopTracking, textAlign: TextAlign.center) ], ); } else { @@ -135,10 +130,10 @@ class UserThingy extends StatelessWidget { onPressed: () { teto.addPlayerToTrack(player).then((value) => setState()); teto.storeState(player); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Added to tracking list!"))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(t.becameTracked))); }, ), - const Text("Track") + Text(t.track, textAlign: TextAlign.center) ], ); } @@ -157,7 +152,7 @@ class UserThingy extends StatelessWidget { ); }, ), - const Text("Compare") + Text(t.compare, textAlign: TextAlign.center) ], ) ]), @@ -173,7 +168,7 @@ class UserThingy extends StatelessWidget { children: [ StatCellNum( playerStat: player.level, - playerStatLabel: "XP Level", + playerStatLabel: t.statCellNum.xpLevel, isScreenBig: bigScreen, alertWidgets: [Text("${NumberFormat.decimalPatternDigits(decimalDigits: 2).format(player.xp)} XP", style: const TextStyle(fontFamily: "Eurostile Round Extended"),), Text("Progress to next level: ${((player.level - player.level.floor()) * 100).toStringAsFixed(2)} %"), Text("Progress from 0 XP to level 5000: ${((player.xp / 67009017.7589378) * 100).toStringAsFixed(2)} %")], higherIsBetter: true, @@ -181,32 +176,32 @@ class UserThingy extends StatelessWidget { if (player.gameTime >= Duration.zero) StatCellNum( playerStat: player.gameTime.inHours, - playerStatLabel: "Hours\nPlayed", + playerStatLabel: t.statCellNum.hoursPlayed, isScreenBig: bigScreen, - alertWidgets: [Text("Exact gametime: ${player.gameTime.toString()}")], + alertWidgets: [Text("${t.exactGametime}: ${player.gameTime.toString()}")], higherIsBetter: true,), if (player.gamesPlayed >= 0) StatCellNum( playerStat: player.gamesPlayed, isScreenBig: bigScreen, - playerStatLabel: "Online\nGames", + playerStatLabel: t.statCellNum.onlineGames, higherIsBetter: true,), if (player.gamesWon >= 0) StatCellNum( playerStat: player.gamesWon, isScreenBig: bigScreen, - playerStatLabel: "Games\nWon", + playerStatLabel: t.statCellNum.gamesWon, higherIsBetter: true,), if (player.friendCount > 0) StatCellNum( playerStat: player.friendCount, isScreenBig: bigScreen, - playerStatLabel: "Friends", + playerStatLabel: t.statCellNum.friends, higherIsBetter: true,), ], ) : Text( - "BANNED", + t.bigRedBanned, textAlign: TextAlign.center, style: TextStyle( fontFamily: "Eurostile Round Extended", @@ -217,7 +212,7 @@ class UserThingy extends StatelessWidget { ), if (player.badstanding != null && player.badstanding!) Text( - "BAD STANDING", + t.bigRedBadStanding, textAlign: TextAlign.center, style: TextStyle( fontFamily: "Eurostile Round Extended", @@ -231,7 +226,7 @@ class UserThingy extends StatelessWidget { children: [ Expanded( child: Text( - "${player.country != null ? "${player.country?.toUpperCase()} • " : ""}${player.role.capitalize()} account ${player.registrationTime == null ? "that was from very beginning" : 'created ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " by ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? "Not a supporter" : "Supporter tier ${player.supporterTier}"}", + "${player.country != null ? "${player.country?.toUpperCase()} • " : ""}${t.playerRole[player.role]}${t.playerRoleAccount}${player.registrationTime == null ? t.wasFromBeginning : '${t.created} ${dateFormat.format(player.registrationTime!)}'}${player.botmaster != null ? " ${t.botCreatedBy} ${player.botmaster}" : ""} • ${player.supporterTier == 0 ? t.notSupporter : t.supporter(tier: player.supporterTier)}", textAlign: TextAlign.center, style: const TextStyle( fontFamily: "Eurostile Round", @@ -268,8 +263,8 @@ class UserThingy extends StatelessWidget { children: [ Image.asset("res/tetrio_badges/${badge.badgeId}.png"), Text(badge.ts != null - ? "Obtained ${dateFormat.format(badge.ts!)}" - : "That badge was assigned manualy by TETR.IO admins"), + ? t.obtainDate(date: dateFormat.format(badge.ts!)) + : t.assignedManualy), ], ) ], @@ -277,7 +272,7 @@ class UserThingy extends StatelessWidget { ), actions: [ TextButton( - child: const Text('OK'), + child: Text(t.popupActions.ok), onPressed: () { Navigator.of(context).pop(); }, diff --git a/pubspec.lock b/pubspec.lock index 03a6ef8..0dd6dd4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csv: + dependency: transitive + description: + name: csv + sha256: "016b31a51a913744a0a1655c74ff13c9379e1200e246a03d96c81c5d9ed297b5" + url: "https://pub.dev" + source: hosted + version: "5.0.2" cupertino_icons: dependency: "direct main" description: @@ -230,6 +238,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -276,10 +289,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.18.0" js: dependency: transitive description: @@ -288,6 +301,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json2yaml: + dependency: transitive + description: + name: json2yaml + sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51 + url: "https://pub.dev" + source: hosted + version: "3.0.1" json_annotation: dependency: transitive description: @@ -501,6 +522,22 @@ packages: description: flutter source: sdk version: "0.0.99" + slang: + dependency: "direct main" + description: + name: slang + sha256: a90af3c2a70ae7d302f47717c0578370e5b2e6040c84280c3e11c9221c2a34ae + url: "https://pub.dev" + source: hosted + version: "3.20.0" + slang_flutter: + dependency: "direct main" + description: + name: slang_flutter + sha256: f3fb0ffabc5119dbe39fb8ef134d0415a27b1da816f32f1f55c8b67d4e2ac1af + url: "https://pub.dev" + source: hosted + version: "3.20.0" source_span: dependency: transitive description: @@ -629,6 +666,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 893c402..e418d23 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,18 +2,6 @@ name: tetra_stats description: Track your and other player stats in TETR.IO publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 0.2.0+4 environment: @@ -29,6 +17,8 @@ dependencies: http: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.2 vector_math: any sqflite: ^2.2.8+2 @@ -39,10 +29,12 @@ dependencies: fl_chart: ^0.62.0 package_info_plus: ^4.0.2 shared_preferences: ^2.1.1 - intl: ^0.18.1 + intl: ^0.18.0 syncfusion_flutter_gauges: ^22.1.34 file_selector: ^0.9.4 file_picker: ^5.3.2 + slang: ^3.20.0 + slang_flutter: ^3.20.0 dev_dependencies: flutter_test: @@ -69,6 +61,15 @@ flutter_launcher_icons: generate: true image_path: "res/icons/app.png" +targets: + $default: + builders: + slang_build_runner: + options: + input_directory: res/i18n + output_directory: lib/i18n + + flutter: uses-material-design: true assets: diff --git a/res/i18n/strings.i18n.json b/res/i18n/strings.i18n.json new file mode 100644 index 0000000..4875318 --- /dev/null +++ b/res/i18n/strings.i18n.json @@ -0,0 +1,89 @@ +{ + "locales(map)": { + "en": "English", + "ru": "Russian (Русский)" + }, + "tetraLeague": "Tetra League", + "tlRecords": "TL Records", + "history": "History", + "sprint": "40 Lines", + "blitz": "Blitz", + "other": "Other", + "zen": "Zen", + "bio": "Bio", + "refresh": "Refresh", + "showStoredData": "Show stored data", + "statsCalc": "Stats Calculator", + "settings": "Settings", + "track": "Track", + "stopTracking": "Stop\ntracking", + "becameTracked": "Added to tracking list!", + "compare": "Compare", + "stoppedBeingTracked": "Removed from tracking list!", + "tlLeaderboard": "Tetra League leaderboard", + "noRecords": "No records", + "noRecord": "No record", + "notEnoughData": "Not enough data", + "noHistorySaved": "No history saved", + "obtainDate": "Obtained ${date}", + "fetchDate": "Fetched ${date}", + "exactGametime": "Exact gametime", + "bigRedBanned": "BANNED", + "bigRedBadStanding": "BAD STANDING", + "copiedToClipboard": "Copied to clipboard!", + "playerRoleAccount": " account ", + "wasFromBeginning": "that was from very beginning", + "created": "created", + "botCreatedBy": "by", + "notSupporter": "Not a supporter", + "assignedManualy": "That badge was assigned manualy by TETR.IO admins", + "supporter": "Supporter tier ${tier}", + "comparingWith": "Comparing with data from ${date}", + "top": "Top", + "topRank": "Top Rank", + "decaying": "Decaying", + "gamesUntilRanked": "${left} games until being ranked", + "nerdStats": "Nerd Stats", + "statCellNum":{ + "xpLevel": "XP Level", + "hoursPlayed": "Hours\nPlayed", + "onlineGames": "Online\nGames", + "gamesWon": "Games\nWon", + "friends": "Friends", + "apm": "Attack\nPer Minute", + "vs": "Versus\nScore", + "lbp": "Leaderboard\nplacement", + "lbpc": "Country LB\nplacement", + "gamesPlayed": "Games\nplayed", + "gamesWonTL": "Games\nWon", + "winrate": "Winrate\nprecentage", + "level": "Level", + "score": "Score", + "spp": "Score\nPer Piece", + "pieces": "Pieces\nPlaced", + "pps": "Pieces\nPer Second", + "finesseFaults": "Finesse\nFaults", + "finessePercentage": "Finesse\nPercentage", + "keys": "Key\nPresses", + "kpp": "KP Per\nPiece", + "kps": "KP Per\nSecond" + }, + "playerRole(map)": { + "user": "User", + "banned": "Banned", + "bot": "Bot", + "sysop": "System operator", + "admin": "Admin", + "mod": "Moderator", + "halfmod": "Community moderator" + }, + "numOfGameActions":{ + "pc": "All Clears", + "hold": "Holds", + "tspinsTotal": "T-spins total", + "lineClears": "Line clears" + }, + "popupActions":{ + "ok": "OK" + } + } \ No newline at end of file diff --git a/res/i18n/strings_ru.i18n.json b/res/i18n/strings_ru.i18n.json new file mode 100644 index 0000000..9061626 --- /dev/null +++ b/res/i18n/strings_ru.i18n.json @@ -0,0 +1,89 @@ +{ + "locales(map)": { + "en": "Английский (English)", + "ru": "Русский" + }, + "tetraLeague": "Тетра Лига", + "tlRecords": "Матчи ТЛ", + "history": "История", + "sprint": "40 линий", + "blitz": "Блиц", + "other": "Другое", + "zen": "Дзен", + "bio": "Биография", + "refresh": "Обновить", + "showStoredData": "Показать сохранённые данные", + "statsCalc": "Калькулятор статистики", + "settings": "Настройки", + "track": "Отслеживать", + "stopTracking": "Перестать\nотслеживать", + "becameTracked": "Добавлен в список отслеживания!", + "stoppedBeingTracked": "Удалён из списка отслеживания!", + "compare": "Сравнить", + "tlLeaderboard": "Таблица лидеров Тетра Лиги", + "noRecords": "Нет записей", + "noRecord": "Нет рекорда", + "notEnoughData": "Недостаточно данных", + "noHistorySaved": "Нет сохранённой истории", + "obtainDate": "Получено ${date}", + "fetchDate": "На момент ${date}", + "exactGametime": "Время, проведённое в игре", + "bigRedBanned": "ЗАБАНЕН", + "bigRedBadStanding": "ПЛОХАЯ РЕПУТАЦИЯ", + "copiedToClipboard": "Скопировано в буфер обмена!", + "playerRoleAccount": ", аккаунт которого ", + "wasFromBeginning": "существовал с самого начала", + "created": "создан", + "botCreatedBy": "игроком", + "notSupporter": "Нет саппортерки", + "supporter": "Саппортерка ${tier} уровня", + "assignedManualy": "Этот значок был присвоен вручную администрацией TETR.IO", + "comparingWith": "Сравнивая с данными от ${date}", + "top": "Топ", + "topRank": "Топ Ранг", + "decaying": "Загнивает", + "gamesUntilRanked": "${left} матчей до получения рейтинга", + "nerdStats": "Для задротов", + "statCellNum": { + "xpLevel": "Уровень\nопыта", + "hoursPlayed": "Часов\nСыграно", + "onlineGames": "Онлайн\nИгр", + "gamesWon": "Онлайн\nПобед", + "friends": "Друзей", + "apm": "Атака в\nМинуту", + "vs": "Показатель\nVersus", + "lbp": "Положение\nв рейтинге", + "lbpc": "Положение\nв рейтинге страны", + "gamesPlayed": "Игр\nСыграно", + "gamesWonTL": "Побед", + "winrate": "Процент\nпобед", + "level": "Уровень", + "score": "Счёт", + "spp": "Очков\nна Фигуру", + "pieces": "Фигур\nУстановлено", + "pps": "Фигур в\nСекунду", + "finesseFaults": "Ошибок\nТехники", + "finessePercentage": "% Качества\nТехники", + "keys": "Нажатий\nКлавиш", + "kpp": "Нажатий\nна Фигуру", + "kps": "Нажатий\nв Секунду" + }, + "playerRole(map)": { + "user": "Пользователь", + "banned": "Заблокированный пользователь", + "bot": "Бот", + "sysop": "Системный оператор", + "admin": "Администратор", + "mod": "Модератор", + "halfmod": "Модератор сообщества" + }, + "numOfGameActions":{ + "pc": "Все чисто", + "hold": "В запас", + "tspinsTotal": "T-spins всего", + "lineClears": "Линий очищено" + }, + "popupActions":{ + "ok": "OK" + } + } \ No newline at end of file