From 1caa8c10498c8e277280a032c332649898ae2a60 Mon Sep 17 00:00:00 2001 From: klockky Date: Fri, 3 Apr 2026 21:29:45 +0300 Subject: [PATCH] Added localization support for multiple languages, including Russian and English, and integrated language selection in the login and country selection screens. Updated pubspec.yaml and pubspec.lock to include necessary dependencies. --- l10n.yaml | 4 + .../auth/code_confirmation_screen.dart | 13 +- lib/frontend/screens/auth/login_screen.dart | 244 +++++++-------- .../screens/auth/select_country_screen.dart | 9 +- lib/l10n/app_en.arb | 37 +++ lib/l10n/app_localizations.dart | 296 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 95 ++++++ lib/l10n/app_localizations_ru.dart | 96 ++++++ lib/l10n/app_ru.arb | 37 +++ lib/l10n/terms_of_service.dart | 11 + lib/l10n/tos_en.dart | 96 ++++++ lib/l10n/tos_ru.dart | 101 ++++++ lib/main.dart | 59 +++- pubspec.lock | 13 + pubspec.yaml | 4 + test/widget_test.dart | 44 ++- 16 files changed, 996 insertions(+), 163 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_ru.dart create mode 100644 lib/l10n/app_ru.arb create mode 100644 lib/l10n/terms_of_service.dart create mode 100644 lib/l10n/tos_en.dart create mode 100644 lib/l10n/tos_ru.dart diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..1437ccc --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,4 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-class: AppLocalizations diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 55339be..3840751 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:komet/l10n/app_localizations.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import '../chats/chat_list_screen.dart'; @@ -109,7 +110,10 @@ class _CodeConfirmationScreenState extends State final trackId = result.challengeTrackId; if (trackId == null) { - showCustomNotification(context, 'Ошибка: отсутствуют данные для 2FA'); + showCustomNotification( + context, + AppLocalizations.of(context)!.codeError2faMissing, + ); return; } @@ -141,6 +145,7 @@ class _CodeConfirmationScreenState extends State @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; final hasError = _errorMessage != null; return Scaffold( @@ -170,7 +175,7 @@ class _CodeConfirmationScreenState extends State ), const SizedBox(height: 12), Text( - 'Мы отправили SMS с кодом подтверждения на ваш номер телефона.', + l10n.codeConfirmationSmsSent, style: TextStyle( color: cs.outline, fontSize: 15, @@ -326,8 +331,8 @@ class _CodeConfirmationScreenState extends State ), child: Text( _timerSeconds > 0 - ? 'Отправить повторно через $_timerSeconds сек.' - : 'Отправить код по SMS', + ? l10n.codeResendInSeconds(_timerSeconds) + : l10n.codeResendSms, ), ), ), diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index a72e298..6da104c 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -5,6 +5,8 @@ import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; +import 'package:komet/l10n/app_localizations.dart'; +import 'package:komet/l10n/terms_of_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'code_confirmation_screen.dart'; import 'select_country_screen.dart'; @@ -77,8 +79,82 @@ class _LoginScreenState extends State { } } + String _countryDisplayName(CountryName country) { + final lang = Localizations.localeOf(context).languageCode; + return lang == 'ru' ? country.ru : country.en; + } + + void _showLanguagePicker() { + final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + final appContext = context; + showModalBottomSheet( + context: context, + backgroundColor: cs.surfaceContainerHigh, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + builder: (sheetContext) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24.0, + horizontal: 16.0, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 8), + child: Text( + l10n.loginLanguage, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ListTile( + title: Text( + l10n.languageNameRu, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(sheetContext); + KometApp.stateOf(appContext)?.applyLocale(const Locale('ru')); + }, + ), + ListTile( + title: Text( + l10n.languageNameEn, + style: GoogleFonts.inter( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + onTap: () { + Navigator.pop(sheetContext); + KometApp.stateOf(appContext)?.applyLocale(const Locale('en')); + }, + ), + ], + ), + ), + ); + }, + ); + } + void _showTOS(BuildContext context) { final cs = Theme.of(context).colorScheme; + final termsLocale = Localizations.localeOf(context); showModalBottomSheet( context: context, backgroundColor: cs.surfaceContainerHigh, @@ -107,7 +183,7 @@ class _LoginScreenState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Условия использования', + AppLocalizations.of(context)!.loginTermsOfUse, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 20, @@ -147,99 +223,8 @@ class _LoginScreenState extends State { child: ListView( controller: scrollController, children: [ - //У мя вопрос а тут точно номера статей верные??? Text( - 'Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet"\n\n' - '1. Статус и отношения\n' - '1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX").\n' - '1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа».\n' - '1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям.\n\n' - '2. Условия использования\n' - '2.1. Используя Приложение «Komet», вы:\n' - '• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps)\n' - '• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»;\n' - '• Принимаете на себя все риски, связанные с использованием Приложения.\n' - '2.2. Строго запрещено:\n' - '• Использовать Приложение «Komet» для распространения запрещённого контента;\n' - '• Осуществлять массовые рассылки (спам);\n' - '• Нарушать законодательство РФ и международное право;\n' - '• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX».\n' - '2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ.\n' - '2.4. Особенности технического взаимодействия:\n' - '• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru)\n' - '• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости;\n' - '• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО.\n\n' - '3. Технические аспекты\n' - '3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки.\n' - '3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости.\n' - '3.3. Разработчики не несут ответственности за:\n' - '• Изменения в API оригинального сервиса;\n' - '• Блокировку аккаунтов пользователей;\n' - '• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа».\n\n' - '4. Конфиденциальность\n' - '4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей.\n' - '4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа».\n' - '4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей.\n\n' - '5. Ответственность и ограничения\n' - '5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности.\n' - '5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин.\n\n' - '6. Правовые основания\n' - '6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с:\n' - '• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости;\n' - '• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях;\n' - '• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации;\n' - '• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ.\n' - '6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через:\n' - '• Публичные API‑интерфейсы, доступные через веб‑версию сервиса;\n' - '• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости;\n' - '• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны.\n' - '6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ.\n\n' - '7. Заключительные положения\n' - '7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что:\n' - '• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов;\n' - '• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»;\n' - '• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки.\n' - '7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей.\n\n' - '8. Функции безопасности и конфиденциальности\n' - '8.1. Приложение «Komet» включает инструменты защиты приватности:\n' - '• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT);\n' - '• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия;\n' - '• Ограничение телеметрии — для минимизации передачи диагностических данных.\n' - '8.2. Данные функции:\n' - '• Направлены исключительно на защиту конфиденциальности пользователей;\n' - '• Не используются для обхода систем безопасности оригинального сервиса;\n' - '• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни.\n' - '8.3. Разработчики не несут ответственности за:\n' - '• Блокировки, связанные с использованием инструментов конфиденциальности;\n' - '• Изменения в работе сервиса при активации данных функций.\n' - '8.4. Функции экспорта и импорта сессии\n' - '8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для:\n' - '• Обеспечения переносимости данных между устройствами пользователя\n' - '• Резервного копирования учетных данных\n' - '• Восстановления доступа при утере устройства\n' - '8.4.2. Особенности реализации:\n' - '• Экспорт сессии осуществляется без привязки к номеру телефона\n' - '• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256\n' - '• Ключ шифрования известен только пользователю и не сохраняется в приложении\n' - '8.4.3. Техническая реализация экспорта сессии:\n' - '• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе\n' - '• Используется подмена параметров сессии для сохранения контекста аутентификации\n' - '• Интеграция настроек прокси для обеспечения единой конфигурации подключения\n' - '• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки\n' - '• Серверы оригинального сервиса не получают данных о смене устройства пользователя\n' - '• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг)\n' - '8.4.4. Правовые основания:\n' - '• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта\n' - '• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных)\n' - '• Принцип минимизации данных — сбор только необходимой для работы информации\n' - '• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается)\n' - '• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies)\n' - '• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152)\n' - '8.4.5. Ограничения ответственности:\n' - '• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий\n' - '• Разработчики не имеют доступа к зашифрованным данным сессии\n' - '• Восстановление утерянных паролей невозможно в целях безопасности\n' - '• Ключи шифрования не хранятся в приложении и известны только пользователю', + termsOfServiceBody(termsLocale), style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, @@ -328,6 +313,7 @@ class _LoginScreenState extends State { void _showPhoneConfirmationDialog(String formattedPhone) { final screenContext = context; + final l10n = AppLocalizations.of(screenContext)!; showGeneralDialog( context: screenContext, @@ -358,7 +344,7 @@ class _LoginScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Это правильный номер?', + l10n.loginConfirmPhoneTitle, style: GoogleFonts.inter( color: cs.onSurfaceVariant, fontSize: 14, @@ -383,7 +369,7 @@ class _LoginScreenState extends State { TextButton( onPressed: () => Navigator.pop(context), child: Text( - 'Изменить', + l10n.loginEdit, style: GoogleFonts.inter( color: cs.primary, fontSize: 15, @@ -403,26 +389,24 @@ class _LoginScreenState extends State { fullPhone, ); - if (mounted) { - Navigator.push( - screenContext, - MaterialPageRoute( - builder: (context) => CodeConfirmationScreen( - phoneNumber: - '${_selectedCountry.phoneCode} $formattedPhone', - token: result.token, - ), + if (!screenContext.mounted) return; + Navigator.push( + screenContext, + MaterialPageRoute( + builder: (context) => CodeConfirmationScreen( + phoneNumber: + '${_selectedCountry.phoneCode} $formattedPhone', + token: result.token, ), - ); - } + ), + ); } catch (e) { - if (mounted) { - _showPhoneError(e.toString()); - } + if (!screenContext.mounted) return; + _showPhoneError(e.toString()); } }, child: Text( - 'Готово', + l10n.loginDone, style: GoogleFonts.inter( color: cs.primary, fontSize: 15, @@ -443,7 +427,10 @@ class _LoginScreenState extends State { void _validateAndSubmit() { if (!_isTOSRead) { - showCustomNotification(context, 'Соглащение прочитай щегол'); + showCustomNotification( + context, + AppLocalizations.of(context)!.loginReadTermsNotification, + ); return; } _showPhoneConfirmationDialog(_phoneController.text); @@ -451,6 +438,7 @@ class _LoginScreenState extends State { void _showSecurityOptions(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, backgroundColor: cs.surfaceContainerHigh, @@ -470,7 +458,7 @@ class _LoginScreenState extends State { ListTile( leading: Icon(Symbols.security, color: cs.onSurface), title: Text( - 'Подделка спуфа', + l10n.loginSpoofRedacted, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, @@ -490,7 +478,7 @@ class _LoginScreenState extends State { ListTile( leading: Icon(Symbols.vpn_lock, color: cs.onSurface), title: Text( - 'Прокси', + l10n.loginProxy, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, @@ -511,6 +499,7 @@ class _LoginScreenState extends State { void _showOtherLoginMethods(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, backgroundColor: cs.surfaceContainerHigh, @@ -530,7 +519,7 @@ class _LoginScreenState extends State { ListTile( leading: Icon(Symbols.qr_code_2, color: cs.onSurface), title: Text( - 'По QR code', + l10n.loginSignInWithQr, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, @@ -544,7 +533,7 @@ class _LoginScreenState extends State { ListTile( leading: Icon(Symbols.key, color: cs.onSurface), title: Text( - 'По токену', + l10n.loginSignInWithToken, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, @@ -558,7 +547,7 @@ class _LoginScreenState extends State { ListTile( leading: Icon(Symbols.description, color: cs.onSurface), title: Text( - 'По файлу сессии', + l10n.loginSignInWithSessionFile, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, @@ -580,6 +569,7 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; return Scaffold( backgroundColor: cs.surface, body: GestureDetector( @@ -617,7 +607,7 @@ class _LoginScreenState extends State { ), ), IconButton( - onPressed: () {}, + onPressed: _showLanguagePicker, icon: Icon( Symbols.language, color: cs.onSurfaceVariant, @@ -637,7 +627,7 @@ class _LoginScreenState extends State { ), const SizedBox(height: 16), Text( - 'Войдите в Komet', + l10n.loginTitle, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 32, @@ -646,7 +636,7 @@ class _LoginScreenState extends State { ), const SizedBox(height: 8), Text( - 'Проверьте код страны и введите свой\nномер телефона.', + l10n.loginSubtitle, textAlign: TextAlign.center, style: TextStyle( color: cs.onSurfaceVariant, @@ -663,11 +653,11 @@ class _LoginScreenState extends State { onTap: _showCountryPicker, borderRadius: BorderRadius.circular(50), child: _buildInputField( - label: 'Страна', + label: l10n.loginCountry, content: Row( children: [ Text( - _selectedCountry.ru, + _countryDisplayName(_selectedCountry), style: GoogleFonts.inter( color: cs.onSurface, fontSize: 15, @@ -685,7 +675,7 @@ class _LoginScreenState extends State { ), const SizedBox(height: 40), _buildInputField( - label: 'Номер телефона', + label: l10n.loginPhoneNumber, content: Row( children: [ Text( @@ -717,7 +707,7 @@ class _LoginScreenState extends State { fontWeight: FontWeight.w400, ), decoration: InputDecoration( - hintText: '(000) 000-00-00', + hintText: l10n.loginPhoneHint, hintStyle: TextStyle( color: cs.outline, fontSize: 15, @@ -768,7 +758,7 @@ class _LoginScreenState extends State { TextButton( onPressed: () => _showOtherLoginMethods(context), child: Text( - 'Другие способы входа', + l10n.loginOtherSignInMethods, style: GoogleFonts.inter( color: cs.primary, fontSize: 14, @@ -794,8 +784,7 @@ class _LoginScreenState extends State { ), children: [ TextSpan( - text: - 'Продолжая, вы соглашаетесь с \n', + text: l10n.loginTermsIntro, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 14, @@ -804,8 +793,7 @@ class _LoginScreenState extends State { ), ), TextSpan( - text: - 'пользовательскими соглашениями', + text: l10n.loginTermsLink, style: GoogleFonts.inter( color: cs.primary, fontSize: 14, diff --git a/lib/frontend/screens/auth/select_country_screen.dart b/lib/frontend/screens/auth/select_country_screen.dart index a72335e..3c5f5de 100644 --- a/lib/frontend/screens/auth/select_country_screen.dart +++ b/lib/frontend/screens/auth/select_country_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/countries.dart'; +import 'package:komet/l10n/app_localizations.dart'; class SelectCountryScreen extends StatefulWidget { final CountryName selectedCountry; @@ -41,6 +42,8 @@ class _SelectCountryScreenState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + final lang = Localizations.localeOf(context).languageCode; return Scaffold( backgroundColor: cs.surface, @@ -61,7 +64,7 @@ class _SelectCountryScreenState extends State { fontWeight: FontWeight.w400, ), decoration: InputDecoration( - hintText: 'Поиск страны...', + hintText: l10n.selectCountrySearchHint, hintStyle: GoogleFonts.inter( color: cs.onSurfaceVariant, fontSize: 18, @@ -72,7 +75,7 @@ class _SelectCountryScreenState extends State { onChanged: _filterCountries, ) : Text( - 'Выберите страну', + l10n.selectCountryTitle, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 20, @@ -114,7 +117,7 @@ class _SelectCountryScreenState extends State { ), ), title: Text( - country.ru, + lang == 'ru' ? country.ru : country.en, style: GoogleFonts.inter( color: cs.onSurface, fontSize: 16, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..aa424d8 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,37 @@ +{ + "@@locale": "en", + "loginTitle": "Sign in to Komet", + "loginSubtitle": "Check your country code and enter your\nphone number.", + "loginCountry": "Country", + "loginPhoneNumber": "Phone number", + "loginPhoneHint": "(000) 000-00-00", + "loginOtherSignInMethods": "Other sign-in methods", + "loginTermsIntro": "By continuing, you agree to \n", + "loginTermsLink": "the terms of use", + "loginTermsOfUse": "Terms of use", + "loginConfirmPhoneTitle": "Is this the correct number?", + "loginEdit": "Change", + "loginDone": "Done", + "loginReadTermsNotification": "Please read the terms of use first", + "loginSpoofRedacted": "Spoof redaction", + "loginProxy": "Proxy", + "loginSignInWithQr": "Sign in with QR code", + "loginSignInWithToken": "Sign in with token", + "loginSignInWithSessionFile": "Sign in with session file", + "loginLanguage": "Language", + "languageNameRu": "Русский", + "languageNameEn": "English", + "selectCountryTitle": "Select country", + "selectCountrySearchHint": "Search countries…", + "codeConfirmationSmsSent": "We sent an SMS with a verification code to your phone number.", + "codeResendInSeconds": "Resend in {seconds} s.", + "@codeResendInSeconds": { + "placeholders": { + "seconds": { + "type": "int" + } + } + }, + "codeResendSms": "Resend code via SMS", + "codeError2faMissing": "Error: missing data for 2FA" +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..b0614d5 --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,296 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_ru.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('ru'), + ]; + + /// No description provided for @loginTitle. + /// + /// In en, this message translates to: + /// **'Sign in to Komet'** + String get loginTitle; + + /// No description provided for @loginSubtitle. + /// + /// In en, this message translates to: + /// **'Check your country code and enter your\nphone number.'** + String get loginSubtitle; + + /// No description provided for @loginCountry. + /// + /// In en, this message translates to: + /// **'Country'** + String get loginCountry; + + /// No description provided for @loginPhoneNumber. + /// + /// In en, this message translates to: + /// **'Phone number'** + String get loginPhoneNumber; + + /// No description provided for @loginPhoneHint. + /// + /// In en, this message translates to: + /// **'(000) 000-00-00'** + String get loginPhoneHint; + + /// No description provided for @loginOtherSignInMethods. + /// + /// In en, this message translates to: + /// **'Other sign-in methods'** + String get loginOtherSignInMethods; + + /// No description provided for @loginTermsIntro. + /// + /// In en, this message translates to: + /// **'By continuing, you agree to \n'** + String get loginTermsIntro; + + /// No description provided for @loginTermsLink. + /// + /// In en, this message translates to: + /// **'the terms of use'** + String get loginTermsLink; + + /// No description provided for @loginTermsOfUse. + /// + /// In en, this message translates to: + /// **'Terms of use'** + String get loginTermsOfUse; + + /// No description provided for @loginConfirmPhoneTitle. + /// + /// In en, this message translates to: + /// **'Is this the correct number?'** + String get loginConfirmPhoneTitle; + + /// No description provided for @loginEdit. + /// + /// In en, this message translates to: + /// **'Change'** + String get loginEdit; + + /// No description provided for @loginDone. + /// + /// In en, this message translates to: + /// **'Done'** + String get loginDone; + + /// No description provided for @loginReadTermsNotification. + /// + /// In en, this message translates to: + /// **'Please read the terms of use first'** + String get loginReadTermsNotification; + + /// No description provided for @loginSpoofRedacted. + /// + /// In en, this message translates to: + /// **'Spoof redaction'** + String get loginSpoofRedacted; + + /// No description provided for @loginProxy. + /// + /// In en, this message translates to: + /// **'Proxy'** + String get loginProxy; + + /// No description provided for @loginSignInWithQr. + /// + /// In en, this message translates to: + /// **'Sign in with QR code'** + String get loginSignInWithQr; + + /// No description provided for @loginSignInWithToken. + /// + /// In en, this message translates to: + /// **'Sign in with token'** + String get loginSignInWithToken; + + /// No description provided for @loginSignInWithSessionFile. + /// + /// In en, this message translates to: + /// **'Sign in with session file'** + String get loginSignInWithSessionFile; + + /// No description provided for @loginLanguage. + /// + /// In en, this message translates to: + /// **'Language'** + String get loginLanguage; + + /// No description provided for @languageNameRu. + /// + /// In en, this message translates to: + /// **'Русский'** + String get languageNameRu; + + /// No description provided for @languageNameEn. + /// + /// In en, this message translates to: + /// **'English'** + String get languageNameEn; + + /// No description provided for @selectCountryTitle. + /// + /// In en, this message translates to: + /// **'Select country'** + String get selectCountryTitle; + + /// No description provided for @selectCountrySearchHint. + /// + /// In en, this message translates to: + /// **'Search countries…'** + String get selectCountrySearchHint; + + /// No description provided for @codeConfirmationSmsSent. + /// + /// In en, this message translates to: + /// **'We sent an SMS with a verification code to your phone number.'** + String get codeConfirmationSmsSent; + + /// No description provided for @codeResendInSeconds. + /// + /// In en, this message translates to: + /// **'Resend in {seconds} s.'** + String codeResendInSeconds(int seconds); + + /// No description provided for @codeResendSms. + /// + /// In en, this message translates to: + /// **'Resend code via SMS'** + String get codeResendSms; + + /// No description provided for @codeError2faMissing. + /// + /// In en, this message translates to: + /// **'Error: missing data for 2FA'** + String get codeError2faMissing; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'ru'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'ru': + return AppLocalizationsRu(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..5ddbe9b --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,95 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get loginTitle => 'Sign in to Komet'; + + @override + String get loginSubtitle => + 'Check your country code and enter your\nphone number.'; + + @override + String get loginCountry => 'Country'; + + @override + String get loginPhoneNumber => 'Phone number'; + + @override + String get loginPhoneHint => '(000) 000-00-00'; + + @override + String get loginOtherSignInMethods => 'Other sign-in methods'; + + @override + String get loginTermsIntro => 'By continuing, you agree to \n'; + + @override + String get loginTermsLink => 'the terms of use'; + + @override + String get loginTermsOfUse => 'Terms of use'; + + @override + String get loginConfirmPhoneTitle => 'Is this the correct number?'; + + @override + String get loginEdit => 'Change'; + + @override + String get loginDone => 'Done'; + + @override + String get loginReadTermsNotification => 'Please read the terms of use first'; + + @override + String get loginSpoofRedacted => 'Spoof redaction'; + + @override + String get loginProxy => 'Proxy'; + + @override + String get loginSignInWithQr => 'Sign in with QR code'; + + @override + String get loginSignInWithToken => 'Sign in with token'; + + @override + String get loginSignInWithSessionFile => 'Sign in with session file'; + + @override + String get loginLanguage => 'Language'; + + @override + String get languageNameRu => 'Русский'; + + @override + String get languageNameEn => 'English'; + + @override + String get selectCountryTitle => 'Select country'; + + @override + String get selectCountrySearchHint => 'Search countries…'; + + @override + String get codeConfirmationSmsSent => + 'We sent an SMS with a verification code to your phone number.'; + + @override + String codeResendInSeconds(int seconds) { + return 'Resend in $seconds s.'; + } + + @override + String get codeResendSms => 'Resend code via SMS'; + + @override + String get codeError2faMissing => 'Error: missing data for 2FA'; +} diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..e63e96d --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,96 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Russian (`ru`). +class AppLocalizationsRu extends AppLocalizations { + AppLocalizationsRu([String locale = 'ru']) : super(locale); + + @override + String get loginTitle => 'Войдите в Komet'; + + @override + String get loginSubtitle => + 'Проверьте код страны и введите свой\nномер телефона.'; + + @override + String get loginCountry => 'Страна'; + + @override + String get loginPhoneNumber => 'Номер телефона'; + + @override + String get loginPhoneHint => '(000) 000-00-00'; + + @override + String get loginOtherSignInMethods => 'Другие способы входа'; + + @override + String get loginTermsIntro => 'Продолжая, вы соглашаетесь с \n'; + + @override + String get loginTermsLink => 'пользовательскими соглашениями'; + + @override + String get loginTermsOfUse => 'Условия использования'; + + @override + String get loginConfirmPhoneTitle => 'Это правильный номер?'; + + @override + String get loginEdit => 'Изменить'; + + @override + String get loginDone => 'Готово'; + + @override + String get loginReadTermsNotification => + 'Сначала прочитайте условия использования'; + + @override + String get loginSpoofRedacted => 'Подделка спуфа'; + + @override + String get loginProxy => 'Прокси'; + + @override + String get loginSignInWithQr => 'По QR code'; + + @override + String get loginSignInWithToken => 'По токену'; + + @override + String get loginSignInWithSessionFile => 'По файлу сессии'; + + @override + String get loginLanguage => 'Язык'; + + @override + String get languageNameRu => 'Русский'; + + @override + String get languageNameEn => 'English'; + + @override + String get selectCountryTitle => 'Выберите страну'; + + @override + String get selectCountrySearchHint => 'Поиск страны…'; + + @override + String get codeConfirmationSmsSent => + 'Мы отправили SMS с кодом подтверждения на ваш номер телефона.'; + + @override + String codeResendInSeconds(int seconds) { + return 'Отправить повторно через $seconds сек.'; + } + + @override + String get codeResendSms => 'Отправить код по SMS'; + + @override + String get codeError2faMissing => 'Ошибка: отсутствуют данные для 2FA'; +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..a53ed02 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,37 @@ +{ + "@@locale": "ru", + "loginTitle": "Войдите в Komet", + "loginSubtitle": "Проверьте код страны и введите свой\nномер телефона.", + "loginCountry": "Страна", + "loginPhoneNumber": "Номер телефона", + "loginPhoneHint": "(000) 000-00-00", + "loginOtherSignInMethods": "Другие способы входа", + "loginTermsIntro": "Продолжая, вы соглашаетесь с \n", + "loginTermsLink": "пользовательскими соглашениями", + "loginTermsOfUse": "Условия использования", + "loginConfirmPhoneTitle": "Это правильный номер?", + "loginEdit": "Изменить", + "loginDone": "Готово", + "loginReadTermsNotification": "Сначала прочитайте условия использования", + "loginSpoofRedacted": "Подделка спуфа", + "loginProxy": "Прокси", + "loginSignInWithQr": "По QR code", + "loginSignInWithToken": "По токену", + "loginSignInWithSessionFile": "По файлу сессии", + "loginLanguage": "Язык", + "languageNameRu": "Русский", + "languageNameEn": "English", + "selectCountryTitle": "Выберите страну", + "selectCountrySearchHint": "Поиск страны…", + "codeConfirmationSmsSent": "Мы отправили SMS с кодом подтверждения на ваш номер телефона.", + "codeResendInSeconds": "Отправить повторно через {seconds} сек.", + "@codeResendInSeconds": { + "placeholders": { + "seconds": { + "type": "int" + } + } + }, + "codeResendSms": "Отправить код по SMS", + "codeError2faMissing": "Ошибка: отсутствуют данные для 2FA" +} diff --git a/lib/l10n/terms_of_service.dart b/lib/l10n/terms_of_service.dart new file mode 100644 index 0000000..ec8c52a --- /dev/null +++ b/lib/l10n/terms_of_service.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +import 'tos_en.dart'; +import 'tos_ru.dart'; + +String termsOfServiceBody(Locale locale) { + if (locale.languageCode == 'ru') { + return kTermsOfServiceRu; + } + return kTermsOfServiceEn; +} diff --git a/lib/l10n/tos_en.dart b/lib/l10n/tos_en.dart new file mode 100644 index 0000000..45a3f16 --- /dev/null +++ b/lib/l10n/tos_en.dart @@ -0,0 +1,96 @@ +const String kTermsOfServiceEn = r''' +Terms of use for the unofficial MAX client named "KometClient" or "Komet" + +1. Status and relationships +1.1. "Komet" (the "App") is an unofficial third-party application and is not affiliated with Communication Platform LLC (rights holder of the "MAX" service). +1.2. The App developers are not partners, employees, or affiliates of Communication Platform LLC. +1.3. All references to the "MAX" trademarks and related services belong to their respective owners. + +2. Terms of use +2.1. By using "Komet", you: +• Automatically agree to the official MAX User Agreement (https://legal.max.ru/ps) +• Understand that using an unofficial client may lead to your account being blocked by Communication Platform LLC +• Accept all risks associated with using the App +2.2. Strictly prohibited: +• Using "Komet" to distribute prohibited content +• Sending bulk messages (spam) +• Violating applicable law +• Attempting to hack or disrupt the original "MAX" service +2.3. The technical implementation follows fair-use principles and does not infringe the rights holder's exclusive rights under applicable interoperability and fair-use norms. +2.4. Technical interaction: +• "Komet" uses publicly available interaction methods with "MAX", similar to the web client (https://web.max.ru) +• Requests are made for interoperability purposes +• The developers do not circumvent technical protection measures or decompile the original software + +3. Technical aspects +3.1. "Komet" uses only publicly available methods to interact with "MAX" through official endpoints. +3.2. Requests are made under fair use for interoperability. +3.3. The developers are not liable for: +• Changes to the original service API +• Account blocks +• Functional limitations caused by Communication Platform LLC + +4. Privacy +4.1. "Komet" does not store or process users' personal data on developers' servers. +4.2. Authentication data is sent directly to Communication Platform LLC servers. +4.3. The developers do not have access to logins, passwords, chats, or other personal user data. + +5. Liability and limitations +5.1. "Komet" is provided "as is" without warranties. +5.2. The developers may discontinue support at any time without notice. + +6. Legal basis +6.1. Development and distribution of "Komet" are carried out with regard to interoperability, fair use, and publicly available information principles, including EU Directive 2019/790 on interoperability where applicable. +6.2. Interaction with "MAX" occurs only through: +• Public interfaces available via the web client +• Reverse engineering methods permitted for interoperability purposes where applicable by law +• Open interaction protocols not protected by technical protection measures +6.3. "Komet" does not circumvent technical protection measures or disrupt the normal operation of the original service. + +7. Final provisions +7.1. By using "Komet", you agree that: +• The authorized way to use "MAX" is through official clients +• Service-related claims should be directed to Communication Platform LLC +• The App developers are not liable for any direct or indirect damages +7.2. These terms may change without prior notice. + +8. Security and privacy features +8.1. "Komet" may include privacy tools: +• Session data substitution to reduce tracking via OSINT-style techniques +• Proxy connectivity for safer network access +• Reduced telemetry where applicable +8.2. These features: +• Aim to protect user privacy +• Are not intended to bypass the original service's security systems +• Are implemented with respect for privacy protections under applicable law +8.3. The developers are not liable for: +• Blocks related to privacy tools +• Service behavior when such features are enabled +8.4. Session export and import +8.4.1. "Komet" may allow exporting and importing session data for: +• Moving data between your devices +• Backing up credentials +• Restoring access if a device is lost +8.4.2. Implementation notes: +• Export may be decoupled from the phone number where technically possible +• Session data may be protected with a password and encryption (e.g. AES-256) +• Encryption keys are known only to you and are not stored by the developers +8.4.3. Technical approach: +• Export/import may use authorization tokens for service identification +• Session parameters may be adjusted to preserve authentication context +• Proxy settings may be included for a single connectivity configuration +• Imported sessions may route traffic according to your proxy settings +• The original service may not receive explicit device-change metadata beyond what the protocol normally sends +• Encryption may apply to the full exported package (session + proxy configuration) +8.4.4. Legal considerations: +• Processing is based on your consent where personal data laws require it +• Data minimization: only what is needed for operation +• Token use is not unauthorized access under applicable criminal computer-access laws +• Session handling is analogous to legitimate session persistence (e.g. cookies) +• IP masking can be a lawful way to protect personal data where applicable +8.4.5. Limitations: +• You are responsible for passwords and backups +• Developers cannot access your encrypted session exports +• Lost passwords cannot be recovered by design +• Encryption keys are not stored in the app and are known only to you +'''; diff --git a/lib/l10n/tos_ru.dart b/lib/l10n/tos_ru.dart new file mode 100644 index 0000000..7354d08 --- /dev/null +++ b/lib/l10n/tos_ru.dart @@ -0,0 +1,101 @@ +const String kTermsOfServiceRu = r''' +Условия использования неофициального клиента на MAX, именуемым "KometClient" или же "Komet" + +1. Статус и отношения +1.1. «Komet» (далее — «Приложение») — неофициальное стороннее приложение, не имеющее отношения к ООО «Коммуникационная платформа" (правообладатель сервиса «MAX"). +1.2. Разработчики Приложения не являются партнёрами, сотрудниками или аффилированными лицами ООО «Коммуникационная платформа». +1.3. Все упоминания торговых марок «MAX» и связанных сервисов принадлежат их правообладателям. + +2. Условия использования +2.1. Используя Приложение «Komet», вы: +• Автоматически подтверждаете согласие с официальным Пользовательским соглашением «MAX» (https://legal.max.ru/ps) +• Осознаёте, что использование неофициального клиента может привести к блокировке аккаунта со стороны ООО «Коммуникационная платформа»; +• Принимаете на себя все риски, связанные с использованием Приложения. +2.2. Строго запрещено: +• Использовать Приложение «Komet» для распространения запрещённого контента; +• Осуществлять массовые рассылки (спам); +• Нарушать законодательство РФ и международное право; +• Предпринимать попытки взлома или нарушения работы оригинального сервиса «MAX». +2.3. Техническая реализация соответствует принципу добросовестного использования (свободное использование) и не нарушает исключительные права правообладателя в соответствии с статьёй 1273 ГК РФ. +2.4. Особенности технического взаимодействия: +• Приложение «Komet» использует публично доступные методы взаимодействия с сервисом «MAX», аналогичные веб-версии (https://web.max.ru) +• Все запросы выполняются в рамках добросовестного использования для обеспечения совместимости; +• Разработчики не осуществляют обход технических средств защиты и не декомпилируют оригинальное ПО. + +3. Технические аспекты +3.1. Приложение «Komet» использует только публично доступные методы взаимодействия с сервисом «MAX» через официальные конечные точки. +3.2. Все запросы выполняются в рамках добросовестного использования (fair use) для обеспечения совместимости. +3.3. Разработчики не несут ответственности за: +• Изменения в API оригинального сервиса; +• Блокировку аккаунтов пользователей; +• Функциональные ограничения, вызванные действиями ООО «Коммуникационная платформа». + +4. Конфиденциальность +4.1. Приложение «Komet» не хранит и не обрабатывает персональные данные пользователей. +4.2. Все данные авторизации передаются напрямую серверам ООО «Коммуникационная платформа». +4.3. Разработчики не имеют доступа к логинам, паролям, переписке и другим персональным данным пользователей. + +5. Ответственность и ограничения +5.1. Приложение «Komet» предоставляется «как есть» (as is) без гарантий работоспособности. +5.2. Разработчики вправе прекратить поддержку Приложения в любой момент без объяснения причин. + +6. Правовые основания +6.1. Разработка и распространение Приложения «Komet» осуществляются в соответствии с: +• Статья 1280.3 ГК РФ — декомпилирование программы для обеспечения совместимости; +• Статья 1229 ГК РФ — ограничения исключительного права в информационных целях; +• Федеральный закон № 149‑ФЗ «Об информации» — использование общедоступной информации; +• Право на межоперабельность (Directive (EU) 2019/790) — обеспечение взаимодействия программ. +6.2. Взаимодействие с сервисом «MAX» осуществляется исключительно через: +• Публичные API‑интерфейсы, доступные через веб‑версию сервиса; +• Методы обратной разработки, разрешённые ст. 1280.3 ГК РФ для целей совместимости; +• Открытые протоколы взаимодействия, не защищённые техническими средствами охраны. +6.3. Приложение «Komet» не обходит технические средства защиты и не нарушает нормальную работу оригинального сервиса, что соответствует требованиям статьи 1299 ГК РФ. + +7. Заключительные положения +7.1. Используя Приложение «Komet», вы соглашаетесь с тем, что: +• Единственным правомочным способом использования сервиса «MAX» является применение официальных клиентов; +• Все претензии по работе сервиса должны направляться в ООО «Коммуникационная платформа»; +• Разработчики Приложения не несут ответственности за любые косвенные или прямые убытки. +7.2. Настоящее соглашение может быть изменено без предварительного уведомления пользователей. + +8. Функции безопасности и конфиденциальности +8.1. Приложение «Komet» включает инструменты защиты приватности: +• Подмена данных сессии — для предотвращения отслеживания пользователя с помощью продвинутых инструментов Open‑Source‑Intelligence (OSINT); +• Система прокси‑подключений — для обеспечения безопасности сетевого взаимодействия; +• Ограничение телеметрии — для минимизации передачи диагностических данных. +8.2. Данные функции: +• Направлены исключительно на защиту конфиденциальности пользователей; +• Не используются для обхода систем безопасности оригинального сервиса; +• Реализованы в рамках статьи 152.1 ГК РФ о защите частной жизни. +8.3. Разработчики не несут ответственности за: +• Блокировки, связанные с использованием инструментов конфиденциальности; +• Изменения в работе сервиса при активации данных функций. +8.4. Функции экспорта и импорта сессии +8.4.1. Приложение «Komet» предоставляет возможность экспорта и импорта данных сессии для: +• Обеспечения переносимости данных между устройствами пользователя +• Резервного копирования учетных данных +• Восстановления доступа при утере устройства +8.4.2. Особенности реализации: +• Экспорт сессии осуществляется без привязки к номеру телефона +• Данные сессии защищаются паролем и шифрованием по алгоритмам AES‑256 +• Ключ шифрования известен только пользователю и не сохраняется в приложении +8.4.3. Техническая реализация экспорта сессии: +• Экспорт сессии осуществляется через токен авторизации для идентификации в сервисе +• Используется подмена параметров сессии для сохранения контекста аутентификации +• Интеграция настроек прокси для обеспечения единой конфигурации подключения +• Импортированная сессия маскирует источник подключения через указанные прокси‑настройки +• Серверы оригинального сервиса не получают данных о смене устройства пользователя +• Шифрование применяется ко всему пакету данных (сессия + прокси‑конфиг) +8.4.4. Правовые основания: +• Статья 6 ФЗ‑152 «О персональных данных» — обработка данных с согласия субъекта +• Статья 434 ГК РФ — право на выбор формы сделки (электронная форма хранения учетных данных) +• Принцип минимизации данных — сбор только необходимой для работы информации +• Использование токена не является несанкционированным доступом (ст. 272 УК РФ не нарушается) +• Подмена сессии — легитимный метод сохранения аутентификации (аналог браузерных cookies) +• Маскировка IP‑адреса — законный способ защиты персональных данных (ст. 6 ФЗ‑152) +8.4.5. Ограничения ответственности: +• Пользователь самостоятельно несет ответственность за сохранность пароля и резервных копий +• Разработчики не имеют доступа к зашифрованным данным сессии +• Восстановление утерянных паролей невозможно в целях безопасности +• Ключи шифрования не хранятся в приложении и известны только пользователю +'''; diff --git a/lib/main.dart b/lib/main.dart index c8fb446..1e20ee2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:komet/l10n/app_localizations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'backend/api.dart'; import 'backend/modules/account.dart'; import 'backend/modules/messages.dart'; @@ -13,19 +15,65 @@ final api = Api(); final accountModule = AccountModule(api); final messagesModule = MessagesModule(api); +Future _loadInitialLocale() async { + final prefs = await SharedPreferences.getInstance(); + final code = prefs.getString('app_locale'); + if (code != null && (code == 'en' || code == 'ru')) { + return Locale(code); + } + final platform = WidgetsBinding.instance.platformDispatcher.locale; + if (platform.languageCode == 'en' || platform.languageCode == 'ru') { + return Locale(platform.languageCode); + } + return const Locale('ru'); +} + void main() async { WidgetsFlutterBinding.ensureInitialized(); await AppDatabase.init(); await api.connect(); - runApp(const MyApp()); + final initialLocale = await _loadInitialLocale(); + runApp(KometApp(initialLocale: initialLocale)); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class KometApp extends StatefulWidget { + const KometApp({super.key, required this.initialLocale}); + final Locale initialLocale; + + static KometAppState? stateOf(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + State createState() => KometAppState(); +} + +class KometAppState extends State { static const _fallbackSeed = Color(0xFFC1C4FF); - static ColorScheme _adjustScheme(ColorScheme base) { + late Locale _locale; + + @override + void initState() { + super.initState(); + _locale = widget.initialLocale; + } + + Future applyLocale(Locale locale) async { + if (!AppLocalizations.supportedLocales.any( + (l) => l.languageCode == locale.languageCode, + )) { + return; + } + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('app_locale', locale.languageCode); + if (mounted) { + setState(() => _locale = locale); + } + } + + ColorScheme _adjustScheme(ColorScheme base) { return base.copyWith( surface: Color.alphaBlend( base.primary.withValues(alpha: 0.05), @@ -58,6 +106,9 @@ class MyApp extends StatelessWidget { return MaterialApp( title: 'Komet', debugShowCheckedModeBanner: false, + locale: _locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, theme: ThemeData( useMaterial3: true, colorScheme: darkScheme, diff --git a/pubspec.lock b/pubspec.lock index 8948ca6..e8fa11e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -142,6 +142,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -248,6 +253,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 244e464..2dd41b7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter + intl: any # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -65,6 +68,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in diff --git a/test/widget_test.dart b/test/widget_test.dart index b921755..b382eb1 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,30 +1,26 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:komet/main.dart'; +import 'package:komet/l10n/app_localizations.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + testWidgets('AppLocalizations Russian login title', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('ru'), + home: Builder( + builder: (context) { + return Scaffold( + body: Text(AppLocalizations.of(context)!.loginTitle), + ); + }, + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.textContaining('Komet'), findsOneWidget); }); }