diff --git a/.github/workflows/push_check_analyze.yml b/.github/workflows/push_check_analyze.yml new file mode 100644 index 0000000..4a60f1e --- /dev/null +++ b/.github/workflows/push_check_analyze.yml @@ -0,0 +1,67 @@ +name: Push Analyze Check + +on: + push: + branches: + - '**' + +jobs: + push_check_analyze: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: flutter analyze + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Analysis Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Analysis Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Analysis Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_analyze.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/push_check_publish_dry_run.yml b/.github/workflows/push_check_publish_dry_run.yml new file mode 100644 index 0000000..6992568 --- /dev/null +++ b/.github/workflows/push_check_publish_dry_run.yml @@ -0,0 +1,67 @@ +name: Push Publish Dry Run Check + +on: + push: + branches: + - '**' + +jobs: + push_check_publish_dry_run: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: dart pub publish --dry-run + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Dry Publish Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Dry Publish Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Dry Publish Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_publish_dry_run.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/push_check_test.yml b/.github/workflows/push_check_test.yml new file mode 100644 index 0000000..a603348 --- /dev/null +++ b/.github/workflows/push_check_test.yml @@ -0,0 +1,73 @@ +name: Push Test Check + +on: + push: + branches: + - '**' + +jobs: + push_check_test: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18.x' + + - name: Install Landa Messenger CLI + run: npm install @landamessenger/landa-messenger-api -g + + - uses: subosito/flutter-action@v1 + with: + channel: 'stable' + flutter-version: '3.24.3' + + - run: flutter pub get + + - run: flutter test --coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage/lcov.info + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Test Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check_test.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Test Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Test Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/push_check.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + diff --git a/.github/workflows/tag_version_and_publish.yml b/.github/workflows/tag_version_and_publish.yml new file mode 100644 index 0000000..02d9c24 --- /dev/null +++ b/.github/workflows/tag_version_and_publish.yml @@ -0,0 +1,116 @@ +name: Tag Version and Publish on Push to Master + +on: + push: + branches: + - master + +jobs: + tag_version_and_publish: + + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Read version from pubspec.yml + id: read_version + run: | + VERSION=$(grep '^version: ' pubspec.yaml | cut -d ' ' -f 2) + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Create tag + id: create_tag + run: | + # Checks if the tag already exists in the remote repository + if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then + echo "Error: Tag v${{ env.VERSION }} already exists." + exit 1 + fi + + # Check if the version was found + if [ -z "${{ env.VERSION }}" ]; then + echo "Error: No version found in pubspec.yml" + exit 1 + fi + + git tag "v${{ env.VERSION }}" + git push origin "v${{ env.VERSION }}" + + - name: Handle job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Creation Tag Failed" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Creation Tag Canceled" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Creation Tag Passed" \ + --body "${{ github.repository }}: Tag v${{ env.VERSION }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + + - run: flutter pub get + + - run: dart pub publish --dry-run + + - run: dart pub publish -f + + - name: Handle publish job completion + if: always() + run: | + if [ "${{ job.status }}" == "failure" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🔴 Pub Publish Failed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + elif [ "${{ job.status }}" == "cancelled" ]; then + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟠 Pub Publish Canceled" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + else + landa-messenger-api chat-send \ + --id "${{ secrets.CHAT_ID }}" \ + --api_key "${{ secrets.CHAT_KEY }}" \ + --title "🟢 Pub Publish Passed" \ + --body "${{ github.repository }}: ${{ github.event.head_commit.message }}" \ + --url "https://github.com/stringcare/stringcare/actions/workflows/tag_version_and_publish.yml" \ + --image "https://avatars.githubusercontent.com/u/63705403?s=200&v=4" \ + --background_color "#55000000" \ + --text_color "#FFFFFFFF" + fi + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de9d01..36eb3db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.0 + +* Stable version. Added `disableNative` and `useEncrypted` for testing purposes. + ## 0.1.7 * Dependencies updated diff --git a/android/build.gradle b/android/build.gradle index 3a6d3cf..2287c72 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.stringcare' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index d95065e..f512266 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -39,8 +39,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.stringcare_example" - minSdkVersion 16 - targetSdkVersion 33 + minSdkVersion flutter.minSdkVersion + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/build.gradle b/example/android/build.gradle index 31e9577..86a6168 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/lib/r.dart b/example/lib/r.dart index 04e4a39..69a908a 100644 --- a/example/lib/r.dart +++ b/example/lib/r.dart @@ -1,27 +1,24 @@ /// Autogenerated file. Run the obfuscation command for refresh: -/// +/// /// flutter pub run stringcare:obfuscate -/// +/// // ignore_for_file: non_constant_identifier_names class Assets { Assets(); final String images_coding_svg = 'assets/images/coding.svg'; - final String images_voyager_jpeg = 'assets/images/voyager.jpeg'; - + final String images_voyager_jpeg = 'assets/images/voyager.jpeg'; } class Strings { Strings(); final String hello_there = 'hello_there'; - final String hello_format = 'hello_format'; - + final String hello_format = 'hello_format'; } class R { static Assets assets = Assets(); static Strings strings = Strings(); } - \ No newline at end of file diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index a9e49ee..fc5222b 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -17,8 +17,8 @@ void main() { // Verify that platform version is retrieved. expect( find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data?.startsWith('Running on:') == true, + (Widget widget) => + widget is Text && widget.data?.startsWith('Running on:') == true, ), findsOneWidget, ); diff --git a/lib/src/i18n/app_localizations.dart b/lib/src/i18n/app_localizations.dart index 6827045..aee053f 100644 --- a/lib/src/i18n/app_localizations.dart +++ b/lib/src/i18n/app_localizations.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:stringcare/stringcare.dart'; -class AppLocalizations { +import 'app_localizations_interface.dart'; + +class AppLocalizations extends AppLocalizationsInterface { final Locale systemLocale; final dividerA = '-'; @@ -48,6 +50,7 @@ class AppLocalizations { late Map _localizedStrings; + @override Future load() => _loadLanguage( locale.languageCode, locale.countryCode ?? '', @@ -61,10 +64,10 @@ class AppLocalizations { var data; if (countryCode.isNotEmpty) { data = await rootBundle - .load('${Stringcare().langPath}/${languageCode}_$countryCode.json'); + .load('${Stringcare().languageOrigin}/${languageCode}_$countryCode.json'); } else { data = await rootBundle - .load('${Stringcare().langPath}/$languageCode.json'); + .load('${Stringcare().languageOrigin}/$languageCode.json'); } var jsonRevealed = Stringcare().revealData(data.buffer.asUint8List()); if (jsonRevealed != null) { @@ -78,7 +81,7 @@ class AppLocalizations { } catch (e) { try { var data = await rootBundle.load( - '${Stringcare().langPath}/$languageCode.json', + '${Stringcare().languageOrigin}/$languageCode.json', ); var jsonRevealed = Stringcare().revealData(data.buffer.asUint8List()); if (jsonRevealed != null) { @@ -154,15 +157,15 @@ class AppLocalizations { var data; if (country.isNotEmpty) { data = await rootBundle - .load('${Stringcare().langPath}/${lang}_$country.json'); + .load('${Stringcare().languageOrigin}/${lang}_$country.json'); } else { - data = await rootBundle.load('${Stringcare().langPath}/$lang.json'); + data = await rootBundle.load('${Stringcare().languageOrigin}/$lang.json'); } var jsonRevealed = Stringcare().revealData(data.buffer.asUint8List())!; jsonMap = json.decode(utf8.decode(jsonRevealed, allowMalformed: true)); } catch (e) { try { - var data = await rootBundle.load('${Stringcare().langPath}/$lang.json'); + var data = await rootBundle.load('${Stringcare().languageOrigin}/$lang.json'); var jsonRevealed = Stringcare().revealData(data.buffer.asUint8List())!; jsonMap = json.decode(utf8.decode(jsonRevealed, allowMalformed: true)); } catch (e) { @@ -190,6 +193,7 @@ class AppLocalizations { } // This method will be called from every widget which needs a localized text + @override String? translate(String key, {List? values}) { if (!_localizedStrings.containsKey(key)) { return ""; @@ -206,6 +210,7 @@ class AppLocalizations { } } + @override String getLang() { if (delegate.isSupported(locale)) { return "${locale.languageCode}_${locale.countryCode}"; diff --git a/lib/src/i18n/app_localizations_interface.dart b/lib/src/i18n/app_localizations_interface.dart new file mode 100644 index 0000000..471f9f4 --- /dev/null +++ b/lib/src/i18n/app_localizations_interface.dart @@ -0,0 +1,7 @@ +abstract class AppLocalizationsInterface { + Future load(); + + String getLang(); + + String? translate(String key, {List? values}); +} diff --git a/lib/src/i18n/app_localizations_test.dart b/lib/src/i18n/app_localizations_test.dart new file mode 100644 index 0000000..9f5e2ae --- /dev/null +++ b/lib/src/i18n/app_localizations_test.dart @@ -0,0 +1,252 @@ +import 'dart:io'; +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stringcare/stringcare.dart'; + +import 'app_localizations_interface.dart'; + +class AppLocalizationsTest extends AppLocalizationsInterface { + final Locale systemLocale; + + final dividerA = '-'; + + final dividerB = '_'; + + Locale get locale { + final locale = Stringcare().withLocale(); + if (locale != null) { + return locale; + } + final lang = Stringcare().withLang(); + if (lang != null) { + if (lang.contains(dividerA)) { + String language = lang.split(dividerA)[0].toLowerCase(); + String country = lang.split(dividerA)[1].toUpperCase(); + return Locale(language, country); + } else if (lang.contains(dividerB)) { + String language = lang.split(dividerB)[0].toLowerCase(); + String country = lang.split(dividerB)[1].toUpperCase(); + return Locale(language, country); + } else if (lang.isNotEmpty) { + return Locale(lang.toLowerCase()); + } + } + return systemLocale; + } + + AppLocalizationsTest(this.systemLocale); + + // Helper method to keep the code in the widgets concise + // Localizations are accessed using an InheritedWidget "of" syntax + static AppLocalizationsTest? of(BuildContext context) { + return Localizations.of( + context, AppLocalizationsTest); + } + + // Static member to have a simple access to the delegate from the MaterialApp + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegateTest(); + + late Map _localizedStrings; + + @override + Future load() => _loadLanguage( + locale.languageCode, + locale.countryCode ?? '', + ); + + Future _loadLanguage(String languageCode, String countryCode) async { + // Load the language JSON file from the "lang" folder + Map? jsonMap; + + try { + var data; + if (countryCode.isNotEmpty) { + final file = File('${Stringcare().languageOrigin}/${languageCode}_$countryCode.json'); + data = file.readAsBytesSync(); + } else { + final file = File('${Stringcare().languageOrigin}/${languageCode}.json'); + data = file.readAsBytesSync(); + } + jsonMap = json.decode( + utf8.decode( + data, + allowMalformed: true, + ), + ); + } catch (e) { + try { + final file = File('${Stringcare().languageOrigin}/$languageCode.json'); + jsonMap = json.decode( + utf8.decode( + file.readAsBytesSync(), + allowMalformed: true, + ), + ); + } catch (e) { + _localizedStrings = Map(); + } + } + + _localizedStrings = jsonMap?.map( + (key, value) { + return MapEntry(key, value.toString()); + }, + ) ?? + Map(); + + var asyncLoad = false; + + var language = + RemoteLanguages().localizedStrings['$languageCode-$countryCode'] ?? + RemoteLanguages().localizedStrings['$languageCode'] ?? + Map(); + + if (language.isNotEmpty) { + for (var entry in language.entries.toList()) { + _localizedStrings[entry.key] = language[entry.key] ?? ''; + } + asyncLoad = true; + } + + if (!asyncLoad) { + for (var lang in RemoteLanguages().localizedStrings.keys.toList()) { + if (lang.contains('$languageCode-')) { + var language = RemoteLanguages().localizedStrings[lang] ?? Map(); + for (var entry in language.entries.toList()) { + _localizedStrings[entry.key] = language[entry.key] ?? ''; + } + break; + } + } + } + + return true; + } + + static Future sTranslate( + String language, + String key, { + List? values, + }) async { + // Load the language JSON file from the "lang" folder + var lang; + var country = ""; + if (language.contains("-")) { + lang = language.split("-")[0]; + country = language.split("-")[1]; + } else if (language.contains("_")) { + lang = language.split("_")[0]; + country = language.split("_")[1]; + } else { + lang = language; + } + + Map? jsonMap; + + try { + var data; + if (country.isNotEmpty) { + data = await rootBundle + .load('${Stringcare().languageOrigin}/${lang}_$country.json'); + } else { + data = + await rootBundle.load('${Stringcare().languageOrigin}/$lang.json'); + } + jsonMap = json + .decode(utf8.decode(data.buffer.asUint8List(), allowMalformed: true)); + } catch (e) { + try { + var data = + await rootBundle.load('${Stringcare().languageOrigin}/$lang.json'); + jsonMap = json.decode( + utf8.decode(data.buffer.asUint8List(), allowMalformed: true)); + } catch (e) { + return ""; + } + } + + Map _localizedStrings = jsonMap!.map((key, value) { + return MapEntry(key, value.toString()); + }); + + if (!_localizedStrings.containsKey(key)) { + return ""; + } + + if (values == null || values.isEmpty) { + return _localizedStrings[key]; + } else { + var base = _localizedStrings[key]; + for (var i = 0; i < values.length; i++) { + base = base!.replaceAll("\$${(i + 1).toString()}", values[i]); + } + return base; + } + } + + // This method will be called from every widget which needs a localized text + @override + String? translate(String key, {List? values}) { + if (!_localizedStrings.containsKey(key)) { + return ""; + } + + if (values == null || values.isEmpty) { + return _localizedStrings[key]; + } else { + var base = _localizedStrings[key]; + for (var i = 0; i < values.length; i++) { + base = base!.replaceAll("\$${(i + 1).toString()}", values[i]); + } + return base; + } + } + + @override + String getLang() { + if (delegate.isSupported(locale)) { + return "${locale.languageCode}_${locale.countryCode}"; + } + final defaultLocale = Stringcare().locales[0]; + return "${defaultLocale.languageCode}_${defaultLocale.countryCode}"; + } +} + +class _AppLocalizationsDelegateTest + extends LocalizationsDelegate { + // This delegate instance will never change (it doesn't even have fields!) + // It can provide a constant constructor. + const _AppLocalizationsDelegateTest(); + + @override + bool isSupported(Locale locale) { + for (Locale supportedLocale in Stringcare().locales) { + if (supportedLocale.languageCode == locale.languageCode && + supportedLocale.countryCode == locale.countryCode) { + return true; + } + } + return false; + } + + @override + Future load(Locale locale) async { + AppLocalizationsTest localizations = new AppLocalizationsTest(locale); + RemoteLanguages().localizations = localizations; + await localizations.load(); + return localizations; + } + + @override + bool shouldReload(_AppLocalizationsDelegateTest old) { + if (RemoteLanguages().shouldReload) { + RemoteLanguages().shouldReload = false; + return true; + } + return false; + } +} diff --git a/lib/src/i18n/remote_languages.dart b/lib/src/i18n/remote_languages.dart index da394fe..b52acf5 100644 --- a/lib/src/i18n/remote_languages.dart +++ b/lib/src/i18n/remote_languages.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; - -import 'app_localizations.dart'; +import 'package:stringcare/src/i18n/app_localizations_interface.dart'; class RemoteLanguages { static RemoteLanguages? _instance; @@ -15,7 +14,7 @@ class RemoteLanguages { } Map> localizedStrings = Map(); - AppLocalizations? localizations; + AppLocalizationsInterface? localizations; Future addLanguage(String language, Map values) async { localizedStrings[language] = values; diff --git a/lib/src/widget/sc_asset_image_provider.dart b/lib/src/widget/sc_asset_image_provider.dart index 0bd3745..8184a98 100644 --- a/lib/src/widget/sc_asset_image_provider.dart +++ b/lib/src/widget/sc_asset_image_provider.dart @@ -246,7 +246,7 @@ class ScAssetImageProvider extends ScAssetBundleImageProvider { } @override - int get hashCode => hashValues(keyName, bundle); + int get hashCode => Object.hash(keyName, bundle); @override String toString() => diff --git a/lib/stringcare.dart b/lib/stringcare.dart index d159ebb..9428c78 100644 --- a/lib/stringcare.dart +++ b/lib/stringcare.dart @@ -3,6 +3,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:global_refresh/global_refresh.dart'; import 'package:go_router/go_router.dart'; +import 'package:stringcare/src/compile/stringcare_impl.dart' as compile; +import 'package:stringcare/src/i18n/app_localizations_interface.dart'; +import 'package:stringcare/src/i18n/app_localizations_test.dart'; import 'package:stringcare/src/web/stringcare_impl.dart' if (dart.library.io) 'package:stringcare/src/native/stringcare_impl.dart'; @@ -40,7 +43,18 @@ class Stringcare { return _instance!; } + /// Disables native libs (.so and .dylib) and uses the Dart implementation. + var disableNative = false; + + /// Defines the origin of the text resources. When the encrypted mode is TRUE, + /// the encrypted resources are consumed, otherwise the original unencrypted + /// resources are used. + var useEncrypted = true; + var langPath = "lang"; + var langBasePath = "lang_base"; + + String get languageOrigin => useEncrypted ? langPath : langBasePath; String? Function() withLang = () { return null; @@ -52,16 +66,28 @@ class Stringcare { List locales = [Locale('en')]; - List> delegates = [ + List> commonDelegates = [ FallbackCupertinoLocalisationsDelegate(), - // A class which loads the translations from JSON files - AppLocalizations.delegate, // Built-in localization of basic text for Material widgets GlobalMaterialLocalizations.delegate, // Built-in localization for text direction LTR/RTL GlobalWidgetsLocalizations.delegate, ]; + List> get delegates { + final list = >[]; + for (var delegate in commonDelegates) { + list.add(delegate); + } + // A class which loads the translations from JSON files + if (useEncrypted) { + list.add(AppLocalizations.delegate); + } else { + list.add(AppLocalizationsTest.delegate); + } + return list; + } + final Locale? Function(Locale?, Iterable)? localeResolutionCallback = (locale, supportedLocales) { if (locale == null) return supportedLocales.first; @@ -92,83 +118,67 @@ class Stringcare { return supportedLocales.first; }; - final StringcareCommons api = StringcareImpl(); + final StringcareCommons _productionApi = StringcareImpl(); - Future get platformVersion async { - return api.platformVersion; - } + final StringcareCommons _testApi = compile.StringcareImpl(); - String testHash(List keys) { - return api.testHash(keys); - } + StringcareCommons get _api => disableNative ? _testApi : _productionApi; - String testSign(List keys) { - return api.testSign(keys); - } + Future get platformVersion => _api.platformVersion; - String obfuscate(String value) { - return api.obfuscate(value); - } + String testHash(List keys) => _api.testHash(keys); - String obfuscateWith(List keys, String value) { - return api.obfuscateWith(keys, value); - } + String testSign(List keys) => _api.testSign(keys); - Uint8List? obfuscateData(Uint8List value) { - return api.obfuscateData(value); - } + String obfuscate(String value) => _api.obfuscate(value); - Uint8List? obfuscateDataWith(List keys, Uint8List value) { - return api.obfuscateDataWith(keys, value); - } + String obfuscateWith(List keys, String value) => + _api.obfuscateWith(keys, value); - String reveal(String value) { - return api.reveal(value); - } + Uint8List? obfuscateData(Uint8List value) => _api.obfuscateData(value); - String revealWith(List keys, String value) { - return api.revealWith(keys, value); - } + Uint8List? obfuscateDataWith(List keys, Uint8List value) => + _api.obfuscateDataWith(keys, value); - Uint8List? revealData(Uint8List? value) { - return api.revealData(value); - } + String reveal(String value) => _api.reveal(value); - Uint8List? revealDataWith(List keys, Uint8List value) { - return api.revealDataWith(keys, value); - } + String revealWith(List keys, String value) => + _api.revealWith(keys, value); - String getSignature(List keys) { - return api.getSignature(keys); - } + Uint8List? revealData(Uint8List? value) => _api.revealData(value); - String getSignatureOfValue(String value) { - return api.getSignatureOfValue(value); - } + Uint8List? revealDataWith(List keys, Uint8List value) => + _api.revealDataWith(keys, value); - String getSignatureOfBytes(List data) { - return api.getSignatureOfBytes(data); - } + String getSignature(List keys) => _api.getSignature(keys); - bool validSignature(String signature, List keys) { - return api.validSignature(signature, keys); - } + String getSignatureOfValue(String value) => _api.getSignatureOfValue(value); - String readableObfuscate(String value) { - return api.readableObfuscate(value); - } + String getSignatureOfBytes(List data) => _api.getSignatureOfBytes(data); - String? translate(BuildContext context, String key, {List? values}) { - return AppLocalizations.of(context)!.translate( - key, - values: values, - ); - } + bool validSignature(String signature, List keys) => + _api.validSignature(signature, keys); - Future translateWithLang(String lang, String key, - {List? values}) { - return AppLocalizations.sTranslate(lang, key, values: values); - } + String readableObfuscate(String value) => _api.readableObfuscate(value); + + String? translate( + BuildContext context, + String key, { + List? values, + }) => + appLocalizations?.translate( + key, + values: values, + ); + + Future translateWithLang( + String lang, + String key, { + List? values, + }) => + useEncrypted + ? AppLocalizations.sTranslate(lang, key, values: values) + : AppLocalizationsTest.sTranslate(lang, key, values: values); Future revealAsset(String key) async { var asset = await rootBundle.load(key); @@ -180,14 +190,18 @@ class Stringcare { String? getLangWithContext(BuildContext? context) { if (context == null) return null; - return AppLocalizations.of(context)?.getLang(); + return appLocalizations?.getLang(); } Future load() => loadWithContext(context); + AppLocalizationsInterface? get appLocalizations => useEncrypted + ? AppLocalizations.of(context) + : AppLocalizationsTest.of(context); + Future loadWithContext(BuildContext? context) async { if (context == null) return false; - return AppLocalizations.of(context)?.load() ?? Future.value(false); + return appLocalizations?.load() ?? Future.value(false); } Future refreshWithLangWithContext( diff --git a/pubspec.yaml b/pubspec.yaml index b8fdb9c..9dbdfe5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: stringcare description: Flutter plugin for obfuscate and reveal strings and any other data. -version: 0.1.7 +version: 1.0.0 homepage: https://github.com/StringCare/stringcare repository: https://github.com/StringCare/stringcare @@ -15,11 +15,11 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - crypto: ^3.0.3 + crypto: ^3.0.5 flutter_svg: ^2.0.10+1 # android ios linux macos windows - ffi: ^2.1.2 + ffi: ^2.1.3 global_refresh: ^1.0.0 # android ios linux macos web windows - go_router: ^14.2.0 # android ios linux macos web windows + go_router: ^14.3.0 # android ios linux macos web windows path: ^1.9.0 yaml: ^3.1.2 diff --git a/stringcare.iml b/stringcare.iml deleted file mode 100644 index 90e0f17..0000000 --- a/stringcare.iml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/stringcare_test.dart b/test/stringcare_test.dart index 7fc95c7..0f9805b 100644 --- a/test/stringcare_test.dart +++ b/test/stringcare_test.dart @@ -1,7 +1,5 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stringcare/src/native/stringcare_impl.dart' as native; -import 'package:stringcare/src/web/stringcare_impl.dart' as web; import 'package:stringcare/stringcare.dart'; void main() { @@ -10,40 +8,24 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '42'; - }); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); }); tearDown(() { - channel.setMockMethodCallHandler(null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + null, + ); }); test('getPlatformVersion', () async { - var nativeImpl = native.StringcareImpl(); - var webImpl = web.StringcareImpl(); - print("nativeImpl sign: " + nativeImpl.testSign([])); - print("webImpl sign: " + webImpl.testSign([])); expect(await Stringcare().platformVersion, '42'); }); - - test('init hash match', () async { - var nativeImpl = native.StringcareImpl(); - var webImpl = web.StringcareImpl(); - var nHash = nativeImpl.testHash([]); - var wHash = webImpl.testHash([]); - print("native hash: $nHash"); - print("web hash: $wHash"); - expect(nHash, wHash); - }); - - test('sign match', () async { - var nativeImpl = native.StringcareImpl(); - var webImpl = web.StringcareImpl(); - var nSign = nativeImpl.testSign([]); - var wSign = webImpl.testSign([]); - print("native sign: $nSign"); - print("web sign: $wSign"); - expect(nSign, wSign); - }); }