Skip to content

Commit 9369bcc

Browse files
authored
Real-time preview (crowdin#27)
1 parent 3e884c3 commit 9369bcc

17 files changed

+839
-13
lines changed

README.md

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ The Crowdin Flutter SDK delivers all new translations from Crowdin project to th
2323
## Features
2424

2525
- Load remote strings from Crowdin Over-The-Air Content Delivery Network
26-
- Built-in translations caching mechanism (enabled by default, can be disabled)
27-
- Network usage configuration (All, only Wi-Fi or Cellular)
28-
- Load static strings from the bundled ARB files (usable as a fallback for the CDN strings)
26+
- Built-in translations caching mechanism (enabled by default, can be disabled)
27+
- Network usage configuration (All, only Wi-Fi or Cellular)
28+
- Load static strings from the bundled ARB files (usable as a fallback for the CDN strings)
29+
- Real-Time Preview – all the translations that are done in the Editor can be shown in your version of the application in real-time. View the translations already made and the ones you're currently typing in.
2930

3031
## Requirements
3132

@@ -126,6 +127,121 @@ After receiving the translations, change the app locale as usual and the transla
126127
| `connectionType` | Network type to be used for translations download. Supported values are `any`, `wifi`, `mobileData`, `ethernet` |
127128
| `updatesInterval` | Translations update interval. Translations will not be updated more frequently than the designated time interval (default minimum is 15 minutes). Instead, it will use previously cached translations |
128129

130+
## Real-Time Preview
131+
132+
All translations done in the Crowdin Editor can be displayed in your version of the application in real-time. See the translations that have already been done and the ones you're typing.
133+
134+
> **Note:** Real-Time Preview feature should not be used in production builds.
135+
> Currently, this feature is available only for Android and iOS applications.
136+
137+
### Setup
138+
139+
Add the following code to the Crowdin initialization:
140+
141+
```dart
142+
void main() async {
143+
WidgetsFlutterBinding.ensureInitialized();
144+
145+
await Crowdin.init(
146+
distributionHash: 'distribution_hash',
147+
connectionType: InternetConnectionType.any,
148+
updatesInterval: const Duration(minutes: 15),
149+
withRealTimeUpdates: true, // use this parameter for enable/disable real-time preview functionality
150+
authConfigurations: CrowdinAuthConfig(
151+
clientId: 'clientId', // your clientId from Crowdin OAuth app
152+
clientSecret: 'clientSecret', // your client secret from Crowdin OAuth app
153+
redirectUri: 'redirectUri', // your redirect uri from Crowdin OAuth app
154+
organizationName: 'organizationName' // optional (only for Crowdin Enterprise)
155+
),
156+
);
157+
158+
// ...
159+
}
160+
```
161+
162+
Wrap your app root widget with the `CrowdinRealTimePreviewWidget`:
163+
164+
```dart
165+
@override
166+
Widget build(BuildContext context) {
167+
return CrowdinRealTimePreviewWidget(
168+
child: MaterialApp(
169+
// ...
170+
171+
localizationsDelegates: CrowdinLocalization.localizationsDelegates,
172+
supportedLocales: AppLocalizations.supportedLocales,
173+
),
174+
175+
// ...
176+
);
177+
}
178+
}
179+
```
180+
181+
For [OAuth App](https://support.crowdin.com/creating-oauth-app/) the redirect URL should match your app scheme.
182+
For example, for scheme `<data android:scheme="crowdintest" />`, redirect URL in Crowdin should be `crowdintest://`.
183+
184+
For Android app, declare the following intent filter in `android/app/src/main/AndroidManifest.xml`:
185+
186+
```xml
187+
<manifest ...>
188+
<!-- ... other tags -->
189+
<application ...>
190+
<activity ...>
191+
<!-- ... other tags -->
192+
193+
<intent-filter android:autoVerify="true">
194+
<action android:name="android.intent.action.VIEW" />
195+
<category android:name="android.intent.category.DEFAULT" />
196+
<category android:name="android.intent.category.BROWSABLE" />
197+
<!-- Accepts URIs that begin with https://YOUR_HOST -->
198+
<data android:scheme="[YOUR_SCHEME]"/>
199+
</intent-filter>
200+
201+
</activity>
202+
</application>
203+
</manifest>
204+
```
205+
206+
For iOS app, declare the scheme in `ios/Runner/Info.plist`:
207+
208+
```xml
209+
<?xml ...>
210+
<!-- ... other tags -->
211+
<plist>
212+
<dict>
213+
<!-- ... other tags -->
214+
215+
<key>CFBundleURLTypes</key>
216+
<array>
217+
<dict>
218+
<key>CFBundleURLSchemes</key>
219+
<array>
220+
<string>[YOUR_SCHEME]</string>
221+
</array>
222+
</dict>
223+
</array>
224+
225+
<!-- ... other tags -->
226+
</dict>
227+
</plist>
228+
```
229+
230+
### Config options
231+
232+
| Config option | Description |
233+
|-----------------------|----------------------------------------------------------------------------|
234+
| `withRealTimeUpdates` | Enable Real-Time Preview feature |
235+
| `authConfigurations` | `CrowdinAuthConfig` class that contains parameters for OAuth authorization |
236+
| `clientId` | Crowdin OAuth Client ID |
237+
| `clientSecret` | Crowdin OAuth Client Secret |
238+
| `redirectUri` | Crowdin OAuth redirect URL |
239+
| `organizationName` | An Organization domain name (for Crowdin Enterprise users only) |
240+
241+
For more information about OAuth authorization in Crowdin, please check [this article](https://support.crowdin.com/creating-oauth-app/).
242+
243+
> **Note:** To easily run your app in the Crowdin Editor, you can use [Crowdin Appetize integration](https://store.crowdin.com/appetize-app). It allows your translators to run this app in the Editor, see more context, and provide better translations.
244+
129245
## Notes
130246

131247
- The CDN feature does not update the localization files. if you want to add new translations to the localization files you need to do it yourself.

example/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ android {
4747
applicationId "com.example.example"
4848
// You can update the following values to match your application needs.
4949
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
50-
minSdkVersion flutter.minSdkVersion
50+
minSdkVersion 19
5151
targetSdkVersion flutter.targetSdkVersion
5252
versionCode flutterVersionCode.toInteger()
5353
versionName flutterVersionName

example/lib/main.dart

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:convert';
2+
13
import 'package:crowdin_sdk/crowdin_sdk.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -8,9 +10,18 @@ import 'package:intl/intl.dart';
810
void main() async {
911
WidgetsFlutterBinding.ensureInitialized();
1012
await Crowdin.init(
11-
distributionHash: 'your distribution hash', //your distribution hash
13+
distributionHash: 'distributionHash', //your distribution hash
1214
connectionType: InternetConnectionType.any,
1315
updatesInterval: const Duration(minutes: 25),
16+
17+
//uncomment next lines to enable real-time preview feature
18+
19+
// withRealTimeUpdates: true,
20+
// authConfigurations: CrowdinAuthConfig(
21+
// clientId: 'clientId', // your clientId from Crowdin OAuth app
22+
// clientSecret: 'clientSecret', // your client secret from Crowdin OAuth app
23+
// redirectUri: 'redirectUri', // your redirect uri from Crowdin OAuth app
24+
// ),
1425
);
1526
runApp(const MyHomePage());
1627
}
@@ -23,7 +34,8 @@ class MyHomePage extends StatefulWidget {
2334
}
2435

2536
class _MyHomePageState extends State<MyHomePage> {
26-
Locale currentLocale = Locale(Intl.shortLocale(Intl.systemLocale));
37+
Locale currentLocale = Locale(Intl.shortLocale(Intl
38+
.systemLocale)); //use system locale as default or provide one from your project, e.g. Locale('en')
2739
bool isLoading = true;
2840

2941
@override
@@ -36,7 +48,9 @@ class _MyHomePageState extends State<MyHomePage> {
3648

3749
@override
3850
Widget build(BuildContext context) {
39-
return MaterialApp(
51+
return
52+
// CrowdinRealTimePreviewWidget(child: //uncomment to enable real-time preview feature
53+
MaterialApp(
4054
locale: currentLocale,
4155
localizationsDelegates: CrowdinLocalization.localizationsDelegates,
4256
supportedLocales: AppLocalizations.supportedLocales,
@@ -56,6 +70,7 @@ class _MyHomePageState extends State<MyHomePage> {
5670
})
5771
},
5872
),
73+
// ), //uncomment to enable real-time preview feature
5974
);
6075
}
6176
}
@@ -145,6 +160,11 @@ class _MainScreenState extends State<MainScreen> {
145160
AppLocalizations.of(context)!.counter(_counter),
146161
style: const TextStyle(fontSize: 30),
147162
),
163+
const SizedBox(height: 16),
164+
Text(
165+
AppLocalizations.of(context)!.nThings(_counter, 'Plural example'),
166+
style: const TextStyle(fontSize: 30),
167+
),
148168
],
149169
),
150170
),

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
66
version: 1.0.0+1
77

88
environment:
9-
sdk: '>=2.12.0 <3.0.0'
9+
sdk: '>=2.12.0 <4.0.0'
1010

1111
dependencies:
1212
flutter:

lib/crowdin_sdk.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
library crowdin_sdk;
22

33
export 'src/crowdin.dart';
4+
export 'src/real_time_preview/crowdin_auth_config.dart';
5+
export 'src/real_time_preview/crowdin_real_time_preview_widget.dart';

lib/src/crowdin.dart

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import 'package:crowdin_sdk/src/crowdin_api.dart';
55
import 'package:crowdin_sdk/src/crowdin_storage.dart';
66
import 'package:crowdin_sdk/src/crowdin_extractor.dart';
77
import 'package:crowdin_sdk/src/crowdin_mapper.dart';
8+
import 'package:crowdin_sdk/src/real_time_preview/crowdin_preview_manager.dart';
89
import 'package:flutter/widgets.dart';
910

1011
import 'common/gen_l10n_types.dart';
1112
import 'crowdin_logger.dart';
13+
import 'real_time_preview/crowdin_auth_config.dart';
1214

1315
enum InternetConnectionType { wifi, mobileData, ethernet, any }
1416

@@ -23,7 +25,7 @@ class Crowdin {
2325
static AppResourceBundle? _arb;
2426

2527
@visibleForTesting
26-
set arb(AppResourceBundle? value) {
28+
static set arb(AppResourceBundle? value) {
2729
_arb = value;
2830
}
2931

@@ -35,17 +37,34 @@ class Crowdin {
3537
/// contains certain distribution file paths for locales
3638
static int? _timestamp;
3739

40+
static List<String> _mappingFilePaths = [];
41+
3842
static final CrowdinStorage _storage = CrowdinStorage();
3943

4044
static late int? _timestampCached;
4145

4246
static final _api = CrowdinApi();
4347

48+
static bool _withRealTimeUpdates = false;
49+
50+
static bool get withRealTimeUpdates => _withRealTimeUpdates;
51+
52+
@visibleForTesting
53+
static set withRealTimeUpdates(bool value) {
54+
_withRealTimeUpdates = value;
55+
}
56+
57+
static late CrowdinPreviewManager crowdinPreviewManager;
58+
59+
static late CrowdinAuthConfig? _authConfig;
60+
4461
/// Crowdin SDK initialization
4562
static Future<void> init({
4663
required String distributionHash,
4764
Duration? updatesInterval,
4865
InternetConnectionType? connectionType,
66+
bool withRealTimeUpdates = false,
67+
CrowdinAuthConfig? authConfigurations,
4968
}) async {
5069
await _storage.init();
5170

@@ -75,6 +94,21 @@ class Crowdin {
7594

7695
/// fetch manifest file to check if new updates available
7796
_timestamp = manifest['timestamp'];
97+
98+
_mappingFilePaths = (manifest['mapping'] as List<dynamic>)
99+
.map((e) => e.toString())
100+
.toList();
101+
} else {
102+
CrowdinLogger.printLog(
103+
"something went wrong. Crowdin couldn't download manifest file for your project");
104+
}
105+
106+
_withRealTimeUpdates = withRealTimeUpdates;
107+
108+
_authConfig = authConfigurations;
109+
110+
if (withRealTimeUpdates && _authConfig != null) {
111+
setUpRealTimePreviewManager(_authConfig!);
78112
}
79113
}
80114

@@ -98,6 +132,10 @@ class Crowdin {
98132
distribution = _storage.getTranslationFromStorage(locale);
99133
if (distribution != null) {
100134
_arb = AppResourceBundle(distribution);
135+
if (_withRealTimeUpdates) {
136+
crowdinPreviewManager.setPreviewArb(
137+
_arb!); // set default translations for real-time preview
138+
}
101139
return;
102140
}
103141
}
@@ -134,6 +172,18 @@ class Crowdin {
134172
_arb = null;
135173
return;
136174
}
175+
if (_withRealTimeUpdates) {
176+
crowdinPreviewManager.setPreviewArb(_arb!);
177+
}
178+
}
179+
180+
@visibleForTesting
181+
static void setUpRealTimePreviewManager(CrowdinAuthConfig authConfig) {
182+
crowdinPreviewManager = CrowdinPreviewManager(
183+
config: authConfig,
184+
distributionHash: _distributionHash,
185+
mappingFilePaths: _mappingFilePaths,
186+
);
137187
}
138188

139189
static final Extractor _extractor = Extractor();
@@ -148,7 +198,7 @@ class Crowdin {
148198
try {
149199
return _extractor.getText(
150200
locale,
151-
_arb!,
201+
_withRealTimeUpdates ? crowdinPreviewManager.previewArb : _arb!,
152202
key,
153203
args,
154204
);

lib/src/crowdin_api.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,39 @@ class CrowdinApi {
3434
return null;
3535
}
3636
}
37+
38+
Future<Map<String, dynamic>?> getMapping({
39+
required String distributionHash,
40+
required String mappingFilePath,
41+
}) async {
42+
try {
43+
var response = await http.get(Uri.parse(
44+
'https://distributions.crowdin.net/$distributionHash$mappingFilePath'));
45+
Map<String, dynamic> responseDecoded =
46+
jsonDecode(utf8.decode(response.bodyBytes));
47+
return responseDecoded;
48+
} catch (ex) {
49+
rethrow;
50+
}
51+
}
52+
53+
Future<Map<String, dynamic>?> getMetadata({
54+
required String accessToken,
55+
required String distributionHash,
56+
String? organizationName,
57+
}) async {
58+
try {
59+
String organizationDomain =
60+
organizationName != null ? '$organizationName.' : '';
61+
var response = await http.get(
62+
Uri.parse(
63+
'https://${organizationDomain}api.crowdin.com/api/v2/distributions/metadata?hash=$distributionHash'),
64+
headers: {'Authorization': 'Bearer $accessToken'});
65+
Map<String, dynamic> responseDecoded =
66+
jsonDecode(utf8.decode(response.bodyBytes));
67+
return responseDecoded;
68+
} catch (ex) {
69+
return null;
70+
}
71+
}
3772
}

lib/src/gen/crowdin_generator.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'package:path/path.dart' as path;
77
import '../common/gen_l10n_types.dart';
88
import 'l10n_config.dart';
99

10+
///todo refactor to reuse getKeys in real-time preview
11+
1012
class CrowdinGenerator {
1113
static Future<void> generate() async {
1214
final String projectDirectory = Directory.current.path;

0 commit comments

Comments
 (0)