diff --git a/bin/langsync.exe b/bin/langsync.exe index a49e57f..8ff75f4 100644 Binary files a/bin/langsync.exe and b/bin/langsync.exe differ diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index 16105a2..26068f1 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -13,11 +13,12 @@ import 'package:pub_updater/pub_updater.dart'; const executableName = 'langsync'; const packageName = 'langsync'; -const description = +final description = ''' An AI powered Command Line Interface (CLI) tool that helps you process your original language-specific files such translations, strings & texts.. and generates the corresponding translated files in the target language(s). + ${utils.isDebugMode ? '\n${lightRed.wrap('Debug mode is enabled!')}' : ''} '''; /// {@template langsync_command_runner} diff --git a/lib/src/commands/start_command/start_command.dart b/lib/src/commands/start_command/start_command.dart index 841f67a..43e5544 100644 --- a/lib/src/commands/start_command/start_command.dart +++ b/lib/src/commands/start_command/start_command.dart @@ -94,8 +94,6 @@ class StartCommand extends Command { 'Saving your source file at ${asConfig.sourceFile}..', ); - late Progress localizationProgress; - try { final jsonPartitionRes = await NetClient.instance.savePartitionsJson( apiKey: apiKey, @@ -106,25 +104,15 @@ class StartCommand extends Command { .complete('Your source file has been saved successfully.'); logger - ..info('\n') - ..warn( - 'The ID of this operation is: ${jsonPartitionRes.partitionId}. in case of any issues, please contact us providing this ID so we can help.', - ) - // ..info("\n") - ; - - localizationProgress = logger.customProgress( - 'Starting localization & translation to your target languages..', - ); + ..info('\n') + ..warn( + 'The ID of this operation is: ${jsonPartitionRes.partitionId}. in case of any issues, please contact us providing this ID so we can help.', + ); - final result = await NetClient.instance.startAIProcess( + final result = await aIProcessResult( apiKey: apiKey, - asConfig: asConfig, - jsonPartitionId: jsonPartitionRes.partitionId, - ); - - localizationProgress.complete( - 'Localization operation is completed successfully.', + langs: asConfig.langs, + partitionId: jsonPartitionRes.partitionId, ); logger @@ -150,7 +138,7 @@ class StartCommand extends Command { } catch (e, stacktrace) { logger.customErr( error: e, - progress: localizationProgress, + progress: savingSourceFileProgress, update: 'Something went wrong, try again!', ); @@ -258,4 +246,35 @@ class StartCommand extends Command { ..info('\n') ..success('All files are created successfully.'); } + + Future aIProcessResult({ + required String apiKey, + required Iterable langs, + required String partitionId, + }) async { + final completer = Completer(); + + final processStream = NetClient.instance.startAIProcess( + apiKey: apiKey, + langs: langs, + jsonPartitionId: partitionId, + ); + + LangSyncServerResultSSE? resultSSE; + + processStream.listen( + (event) { + if (event is LangSyncServerResultSSE) { + resultSSE = event; + } else { + logger.info(event.message); + } + }, + onDone: () { + completer.complete(resultSSE!); + }, + ); + + return completer.future; + } } diff --git a/lib/src/etc/models/result_locale.dart b/lib/src/etc/models/result_locale.dart index 0b1839b..3753fce 100644 --- a/lib/src/etc/models/result_locale.dart +++ b/lib/src/etc/models/result_locale.dart @@ -1,21 +1,100 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + import 'package:equatable/equatable.dart'; typedef JsonContentMap = Map; -class LocalizationOutput extends Equatable { +enum LangSyncServerSSEType { info, warn, error, result } + +class LangSyncServerSSE extends Equatable { + final String message; + final LangSyncServerSSEType type; + final int statusCode; + final DateTime date; + + const LangSyncServerSSE({ + required this.message, + required this.type, + required this.statusCode, + required this.date, + }); + @override + List get props => [ + message, + type, + statusCode, + date, + ]; + + factory LangSyncServerSSE.fromJson(Map res) { + final type = LangSyncServerSSEType.values.firstWhere( + (element) => element.name == res['type'] as String, + ); + + if (type == LangSyncServerSSEType.result) { + return LangSyncServerResultSSE.fromJson(res); + } else { + return LangSyncServerSSE( + message: res['message'] as String, + type: type, + statusCode: res['statusCode'] as int, + date: DateTime.parse(res['date'] as String), + ); + } + } +} + +class LangSyncServerResultSSE extends LangSyncServerSSE { final String outputPartitionId; - const LocalizationOutput({ + const LangSyncServerResultSSE({ required this.outputPartitionId, + required super.message, + required super.type, + required super.statusCode, + required super.date, }); - factory LocalizationOutput.fromJson(Map res) { - return LocalizationOutput( - outputPartitionId: res['partitionId'] as String, + factory LangSyncServerResultSSE.fromJson(Map res) { + final message = res['message'] as String; + final decoded = jsonDecode(message) as Map; + + return LangSyncServerResultSSE( + outputPartitionId: decoded['partitionId'] as String, + message: message, + statusCode: res['statusCode'] as int, + type: // this is hardcoded, butsince we are sure that it is correct. + LangSyncServerSSEType.result, + date: DateTime.parse( + res['date'] as String, + ), ); } @override - List get props => [outputPartitionId]; + List get props => [ + outputPartitionId, + super.message, + super.type, + super.statusCode, + super.date, + ]; +} + +class LangSyncServerLoggerSSE extends LangSyncServerSSE { + const LangSyncServerLoggerSSE({ + required super.date, + required super.message, + required super.statusCode, + required super.type, + }); + + @override + List get props => [ + super.date, + super.message, + super.statusCode, + super.type, + ]; } diff --git a/lib/src/etc/networking/client.dart b/lib/src/etc/networking/client.dart index 261a960..4c4a535 100644 --- a/lib/src/etc/networking/client.dart +++ b/lib/src/etc/networking/client.dart @@ -1,17 +1,15 @@ import 'dart:convert'; import 'dart:io'; - -import 'package:http/http.dart' as http; import 'package:langsync/src/etc/models/api_key_res.dart'; import 'package:langsync/src/etc/models/config.dart'; import 'package:langsync/src/etc/models/lang_output.dart'; import 'package:langsync/src/etc/models/partition.dart'; import 'package:langsync/src/etc/models/result_locale.dart'; import 'package:langsync/src/etc/models/user_info.dart'; -import 'package:langsync/src/etc/utils.dart'; +import 'package:langsync/src/etc/networking/client_boilerplate.dart'; import 'package:langsync/src/version.dart'; -class NetClient { +class NetClient extends NetClientBoilerPlate { NetClient._(); final client = HttpClient(); @@ -21,7 +19,7 @@ class NetClient { static NetClient get instance => _instance; Future userInfo({required String apiKey}) { - return _makeRes('user', 'GET', { + return makeRes('user', 'GET', { 'Authorization': 'Bearer $apiKey', }, {}, (res) { return UserInfo.fromJson(res); @@ -29,7 +27,7 @@ class NetClient { } Future> supportsLang(List lang) { - return _makeRes>('/langs-support', 'POST', {}, { + return makeRes>('/langs-support', 'POST', {}, { 'langs': lang, }, (res) { final map = {}; @@ -44,85 +42,26 @@ class NetClient { }); } - Future _makeRes( - String endpoint, - String method, - Map headers, - Map body, - T Function(Map res) onSuccess, - ) async { - final uri = Uri.parse(utils.endpoint(endpoint)); - - final request = http.Request(method, uri); - - request.headers.addAll({ - ...headers, - 'Content-Type': 'application/json', - }); - - // include body in request. - - request.body = json.encode(body); - - final res = await request.send(); - final asBytes = await res.stream.bytesToString(); - - final decoded = jsonDecode(asBytes) as Map; - - if (res.statusCode >= 200 && res.statusCode < 300) { - return onSuccess(decoded); - } else { - throw Exception(decoded); - } - } - - Future _makeMultiPartRes( - String endpoint, - String method, - Map headers, - Map body, - T Function(Map res) onSuccess, - ) async { - final uri = Uri.parse(utils.endpoint(endpoint)); - final request = http.MultipartRequest(method, uri); - - request.headers.addAll({...headers}); - body.forEach((key, value) { - if (value is File) { - final multipartFile = http.MultipartFile.fromBytes( - key, - value.readAsBytesSync(), - filename: value.path.split('/').last, - ); - - request.files.add(multipartFile); - } else { - request.fields[key] = value.toString(); - } - }); - - final res = await request.send(); - final asBytes = await res.stream.bytesToString(); - - return onSuccess(jsonDecode(asBytes) as Map); - } - - Future startAIProcess({ - required LangSyncConfig asConfig, + Stream startAIProcess({ + required Iterable langs, required String apiKey, required String jsonPartitionId, bool includeOutput = false, }) { - return _makeRes( + return sseStreamReq( '/process-translation', 'POST', {'Authorization': 'Bearer $apiKey'}, { 'jsonPartitionsId': jsonPartitionId, - 'langs': asConfig.langs.toList(), + 'langs': langs.toList(), 'includeOutput': includeOutput, }, - LocalizationOutput.fromJson, + (res) { + final decoded = jsonDecode(res) as Map; + + return LangSyncServerSSE.fromJson(decoded); + }, ); } @@ -130,7 +69,7 @@ class NetClient { required String apiKey, required File sourceFile, }) { - return _makeMultiPartRes( + return makeMultiPartRes( '/save-partitioned-json-of-user', 'post', {'Authorization': 'Bearer $apiKey'}, @@ -142,7 +81,7 @@ class NetClient { Future checkWetherApiKeyExistsForSomeUser({ required String apiKey, }) async { - return _makeRes( + return makeRes( '/verify-api-key-existence', 'GET', {'Authorization': 'Bearer $apiKey'}, @@ -163,7 +102,7 @@ class NetClient { Future> retrieveJsonPartitionWithOutput({ required String outputPartitionId, }) { - return _makeRes( + return makeRes( '/get-partitioned-json-of-user', 'GET', {}, @@ -183,7 +122,7 @@ class NetClient { } Future createApiKey(String userName) { - return _makeRes('/create-account-with-api-key-beta', 'POST', {}, { + return makeRes('/create-account-with-api-key-beta', 'POST', {}, { 'username': userName, }, (res) { return APIKeyResponse.fromJson(res); @@ -196,7 +135,7 @@ class NetClient { required String commandName, String? processId, }) { - return _makeRes( + return makeRes( '/log-exception', 'POST', {}, diff --git a/lib/src/etc/networking/client_boilerplate.dart b/lib/src/etc/networking/client_boilerplate.dart new file mode 100644 index 0000000..2e7fc93 --- /dev/null +++ b/lib/src/etc/networking/client_boilerplate.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:http/http.dart' as http; +import 'package:langsync/src/etc/utils.dart'; + +class NetClientBoilerPlate { + Future makeRes( + String endpoint, + String method, + Map headers, + Map body, + T Function(Map res) onSuccess, + ) async { + final uri = Uri.parse(utils.endpoint(endpoint)); + + final request = http.Request(method, uri); + + request.headers.addAll({ + ...headers, + 'Content-Type': 'application/json', + }); + + // include body in request. + + request.body = json.encode(body); + + final res = await request.send(); + final asBytes = await res.stream.bytesToString(); + + final decoded = jsonDecode(asBytes) as Map; + + if (res.statusCode >= 200 && res.statusCode < 300) { + return onSuccess(decoded); + } else { + throw Exception(decoded); + } + } + + Future makeMultiPartRes( + String endpoint, + String method, + Map headers, + Map body, + T Function(Map res) onSuccess, + ) async { + final uri = Uri.parse(utils.endpoint(endpoint)); + final request = http.MultipartRequest(method, uri); + + request.headers.addAll({...headers}); + body.forEach((key, value) { + if (value is File) { + final multipartFile = http.MultipartFile.fromBytes( + key, + value.readAsBytesSync(), + filename: value.path.split('/').last, + ); + + request.files.add(multipartFile); + } else { + request.fields[key] = value.toString(); + } + }); + + final res = await request.send(); + final asBytes = await res.stream.bytesToString(); + + return onSuccess(jsonDecode(asBytes) as Map); + } + + Stream sseStreamReq( + String endpoint, + String method, + Map headers, + Map body, + T Function(String res) onSuccess, + ) { + final _controller = StreamController.broadcast(); + + final uri = Uri.parse(utils.endpoint(endpoint)); + final request = http.Request(method, uri); + + request.headers.addAll({ + ...headers, + 'Content-Type': 'application/json', + }); + + request.body = jsonEncode(body); + + request.send().then( + (streamedRes) { + streamedRes.stream + .transform(utf8.decoder) + // .transform(json.decoder) + .map((data) => onSuccess(data)) + .listen( + (event) { + _controller.sink.add(event); + }, + onError: (e) { + _controller.sink.addError(e as Object); + }, + cancelOnError: true, + onDone: _controller.close, + ); + }, + ); + + return _controller.stream; + } +} diff --git a/lib/src/etc/utils.dart b/lib/src/etc/utils.dart index 4455fba..b08d24c 100644 --- a/lib/src/etc/utils.dart +++ b/lib/src/etc/utils.dart @@ -8,11 +8,11 @@ import 'package:mason_logger/mason_logger.dart'; final utils = Utils(); class Utils { - final isDebugMode = true; + final isDebugMode = !false; //! notice the "/" String get baseUrl => - isDebugMode ? 'http://localhost:5559' : 'https://api.langsync.app'; + isDebugMode ? 'http://192.168.0.5:5559' : 'https://api.langsync.app'; bool isValidApiKeyFormatted(String apiKey) { final isNotEmpty = apiKey.isNotEmpty; diff --git a/lib/src/version.dart b/lib/src/version.dart index 2c37d0f..560322b 100644 --- a/lib/src/version.dart +++ b/lib/src/version.dart @@ -1 +1 @@ -const packageVersion = '0.8.3'; +const packageVersion = '0.9.0'; diff --git a/pubspec.yaml b/pubspec.yaml index dfd53ff..097659a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: langsync description: A Very Good Project created by Very Good CLI. -version: 0.8.3 +version: 0.9.0 publish_to: none environment: