Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
branches: [ main ]

env:
FLUTTER_VERSION: '3.29.3'
FLUTTER_VERSION: '3.38.7'

jobs:
test:
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## 1.0.0

- First stable release

### Breaking Changes

- **Minimum requirements updated**: Dart SDK `>=3.4.0 <4.0.0`, Flutter `>=3.22.0`
- Default `synthetic-package` changed from `true` to `false` to align with Flutter 3.32+ deprecation
- Import paths changed from `package:flutter_gen/gen_l10n/...` to direct source imports

### Migration

1. Add `synthetic-package: false` to your `l10n.yaml` (or rely on new default)
2. Update imports:
```dart
// Before
import 'package:flutter_gen/gen_l10n/crowdin_localizations.dart';

// After
import 'package:your_app/l10n/crowdin_localizations.dart';
```
3. Run `flutter pub run crowdin_sdk:gen` to regenerate

## 0.8.1

- fix: add undeclared placeholders to the placeholders list
Expand Down
28 changes: 12 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</picture>
</p>

# Crowdin Flutter SDK [<img src="https://img.shields.io/badge/beta-yellow"/>](https://github.com/crowdin/flutter-sdk)
# Crowdin Flutter SDK

The Crowdin Flutter SDK enables Over-The-Air (OTA) translation updates, delivering new translations from your Crowdin project directly to users without requiring app store updates. The SDK works on top of Flutter's standard localization system (`flutter_localizations`), providing a seamless bridge between your local ARB files and Crowdin's Content Delivery Network.

Expand Down Expand Up @@ -60,7 +60,8 @@ This architecture ensures your app always has working translations (from local A

## Requirements

* Dart >=2.17.0
* Flutter >=3.22.0
* Dart >=3.4.0

## Setup

Expand All @@ -87,7 +88,7 @@ To manage distributions, open the Crowdin project and go to the *Translations* >

```yml
dependencies:
crowdin_sdk: ^0.8.1
crowdin_sdk: ^1.0.0

flutter_localizations:
sdk: flutter
Expand All @@ -103,16 +104,16 @@ To manage distributions, open the Crowdin project and go to the *Translations* >
flutter pub run crowdin_sdk:gen
```

This generates `crowdin_localizations.dart` in the `{FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n` directory. This wrapper class extends Flutter's generated localization classes to integrate Crowdin OTA translations.
This generates `crowdin_localizations.dart` in the same directory as your `app_localizations.dart` (by default, in your `arb-dir` directory, e.g., `lib/l10n/`). This wrapper class extends Flutter's generated localization classes to integrate Crowdin OTA translations.

> **Important:** Re-run this command whenever you modify the structure of your ARB files (e.g., add/remove keys or change parameters).

- Update localizationsDelegates in your project:

```dart
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:your_app/l10n/app_localizations.dart';
import 'package:crowdin_sdk/crowdin_sdk.dart';
import 'package:flutter_gen/gen_l10n/crowdin_localizations.dart';
import 'package:your_app/l10n/crowdin_localizations.dart';
```

```dart
Expand Down Expand Up @@ -341,15 +342,10 @@ For more information about OAuth authorization in Crowdin, please check [this ar
- Swedish - `sv`: `sv-SE`
- Urdu (India) - `ur`: `ur-IN`

- Since flutter tool no longer generate a synthetic package:flutter_gen, please follow 1st way from [Migration Guide](https://docs.flutter.dev/release/breaking-changes/flutter-generate-i10n-source#migration-guide):
- Specify synthetic-package: false in the accompanying [l10n.yaml](https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#configuring-the-l10n-yaml-file) file:
`synthetic-package: false`

- The files are generated into the path specified by arb-dir
`arb-dir: lib/i18n`

- Or, specifically provide an output path:
`output-dir: lib/src/generated/i18n`
- **synthetic-package default**: Starting from v1.0.0, the SDK defaults to `synthetic-package: false` to align with Flutter 3.32+ deprecation of the synthetic `flutter_gen` package. If you're upgrading from an earlier version, see the [Migration Guide](https://docs.flutter.dev/release/breaking-changes/flutter-generate-i10n-source#migration-guide):
- Add `synthetic-package: false` to your `l10n.yaml` (or rely on the new default)
- Update imports from `package:flutter_gen/gen_l10n/...` to your local path (e.g., `package:your_app/l10n/...`)
- Run `flutter pub run crowdin_sdk:gen` to regenerate

## Contributing

Expand Down
4 changes: 2 additions & 2 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import 'dart:convert';

import 'package:crowdin_sdk/crowdin_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_gen/gen_l10n/crowdin_localizations.dart';
import 'l10n/app_localizations.dart';
import 'l10n/crowdin_localizations.dart';

import 'package:intl/intl.dart';

Expand Down
36 changes: 18 additions & 18 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.8.1"
version: "1.0.0"
crypto:
dependency: transitive
description:
Expand Down Expand Up @@ -132,10 +132,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
version: "1.3.3"
ffi:
dependency: transitive
description:
Expand Down Expand Up @@ -208,34 +208,34 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.8"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.9"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
Expand Down Expand Up @@ -264,10 +264,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
nm:
dependency: transitive
description:
Expand Down Expand Up @@ -445,10 +445,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.4"
version: "0.7.7"
typed_data:
dependency: transitive
description:
Expand Down Expand Up @@ -525,10 +525,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
Expand Down Expand Up @@ -586,5 +586,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0 <4.0.0"
dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.27.0"
2 changes: 1 addition & 1 deletion lib/src/common/gen_l10n_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ class Message {

static final RegExp _pluralRE = RegExp(r'\s*\{([\w\s,]*),\s*plural\s*,');

bool get isPlural => _pluralMatch != null && _pluralMatch!.groupCount == 1;
bool get isPlural => _pluralMatch != null && _pluralMatch.groupCount == 1;

Placeholder getCountPlaceholder() {
assert(isPlural);
Expand Down
4 changes: 2 additions & 2 deletions lib/src/crowdin_request_limiter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class CrowdinRequestLimiter {
bool get pauseRequests =>
_stopPermanently || _pauseRequests || _checkIsPausedForToday();

init(CrowdinStorage storage) {
void init(CrowdinStorage storage) async {
_storage = storage;
_stopPermanently = _storage.getIsPausedPermanently() ?? false;
_errorMap = _storage.getErrorMap() ?? {};
Expand Down Expand Up @@ -59,7 +59,7 @@ class CrowdinRequestLimiter {
_storage.setErrorMap(_cleanErrorMapFromUnusedDays());
}

reset() {
void reset() {
if (!_stopPermanently) {
_pauseRequests = false;
_errorMap = {};
Expand Down
4 changes: 2 additions & 2 deletions lib/src/gen/l10n_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class L10nConfig {
required this.outputLocalizationFile,
required this.outputDir,
required this.outputClass,
this.syntheticPackage = true,
this.syntheticPackage = false,
});

String get finalOutputDir => syntheticPackage
Expand All @@ -41,7 +41,7 @@ class L10nConfig {

String outputClass = yamlGenConfig['output-class'] ?? 'AppLocalizations';

bool syntheticPackage = yamlGenConfig['synthetic-package'] ?? true;
bool syntheticPackage = yamlGenConfig['synthetic-package'] ?? false;

return L10nConfig(
arbDir: arbDir,
Expand Down
9 changes: 4 additions & 5 deletions lib/src/real_time_preview/crowdin_preview_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,11 @@ class CrowdinPreviewManager {
'Something went wrong when subscribing to translations for real-time preview. Metadata is not provided');
} else {
_CrowdinMetadata metadata = _metadata!;
final webSocketTicketEvent =
'update-draft:${metadata.wsHash}:${metadata.projectId}:${metadata.userId}';
final ticket = await _getWebsocketTicket(
credentials: _credentials, event: webSocketTicketEvent);
for (var id in finalMapping.values) {
final String event = '$webSocketTicketEvent:$langCode:$id';
final String event =
'update-draft:${metadata.wsHash}:${metadata.projectId}:${metadata.userId}:$langCode:$id';
final ticket =
await _getWebsocketTicket(credentials: _credentials, event: event);
if (ticket != null) {
var data = jsonEncode({
'action': 'subscribe',
Expand Down
10 changes: 5 additions & 5 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
name: crowdin_sdk
description: Crowdin Flutter SDK for instant translation delivery Over-The-Air directly to your application
version: 0.8.1
version: 1.0.0
repository: https://github.com/crowdin/flutter-sdk
homepage: https://github.com/crowdin/flutter-sdk

environment:
sdk: '>=2.17.0 <4.0.0'
flutter: ">=1.17.0"
sdk: '>=3.4.0 <4.0.0'
flutter: ">=3.22.0"

dependencies:
flutter:
sdk: flutter
http: '>=0.13.3 <2.0.0'
intl: '>=0.17.0 <=0.20.2'
shared_preferences: ^2.0.4
connectivity_plus: '>=2.1.0 <7.0.0'
connectivity_plus: '>=2.1.0 <8.0.0'
yaml: ^3.1.1
meta: ^1.7.0
path: ^1.8.1
oauth2: ^2.0.1
url_launcher: ^6.1.0
app_links: '>=3.4.2 <7.0.0'
app_links: '>=3.4.2 <8.0.0'
web_socket_channel: '>=2.2.0 <4.0.0'

flutter_localizations:
Expand Down
4 changes: 2 additions & 2 deletions test/crowdin_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ void main() {

test('getManifest returns null and increment error count on 400 status',
() async {
await requestLimiter.init(storage);
requestLimiter.init(storage);

final uri =
Uri.parse('https://distributions.crowdin.net/hash/manifest.json');
Expand All @@ -119,7 +119,7 @@ void main() {
'getManifest returns null and do not call request when requests paused',
() async {
storage.setIsPausedPermanently(true);
await requestLimiter.init(storage);
requestLimiter.init(storage);

final uri =
Uri.parse('https://distributions.crowdin.net/hash/manifest.json');
Expand Down
8 changes: 4 additions & 4 deletions test/crowdin_request_limiter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ void main() {

test('should initialize with storage values', () async {
storage.setIsPausedPermanently(true);
await requestLimiter.init(storage);
requestLimiter.init(storage);
expect(storage.getIsPausedPermanently(), true);
expect(requestLimiter.pauseRequests, true);
});
Expand All @@ -49,13 +49,13 @@ void main() {

test('should pause requests after max errors in a day', () async {
storage.setErrorMap({getTodayDateString(): 10});
await requestLimiter.init(storage);
requestLimiter.init(storage);
expect(requestLimiter.pauseRequests, true);
});

test('should reset error map and pause state', () async {
storage.setErrorMap({getTodayDateString(): 10});
await requestLimiter.init(storage);
requestLimiter.init(storage);
expect(requestLimiter.pauseRequests, true);
requestLimiter.reset();
expect(requestLimiter.pauseRequests, false);
Expand All @@ -67,7 +67,7 @@ void main() {
_formatter.format(DateTime.now().subtract(const Duration(days: 1))): 10,
_formatter.format(DateTime.now().subtract(const Duration(days: 2))): 10,
});
await requestLimiter.init(storage);
requestLimiter.init(storage);
requestLimiter.incrementErrorCounter();
expect(requestLimiter.pauseRequests, true);
});
Expand Down