stringjet_ota_sdk 0.2.2
stringjet_ota_sdk: ^0.2.2 copied to clipboard
Flutter SDK wrapper for StringJet OTA translations.
example/lib/main.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'stringjet_ffi.dart';
/// Same demo project as `samples/import-fixtures` (Android / iOS / React samples).
const String kStringJetSdkToken = 'a2dcfedf-dbff-4074-b805-59cac6d7b5e6';
const String kStringJetProjectId = 'e391c041-59e4-436e-a715-79ace250c7f3';
const String kBundledArbAsset = 'assets/import_fixtures/app_en.arb';
const List<String> kFixtureKeys = [
'hard_welcome',
'hard_named',
'hard_percent',
'hard_quote',
'hard_plural_apples',
];
/// Demo values for known ARB placeholder names (matches Android/iOS sample args).
const Map<String, String> kArbDemoPlaceholders = {
'ps1': 'Sam',
'pi2': '3',
'pf3': '19.99',
'user': 'StringJet',
'as0': '7',
'ai0': '7',
'name': 'StringJet',
'count': '3',
};
/// OTA URL the Kotlin/Native FFI layer uses (`PlatformIds.FLUTTER`); log-only aid for debugging.
String expectedFlutterOtaUrl({bool prerelease = false}) {
const base = 'https://cdn.stringjet.com';
final enc = Uri.encodeQueryComponent(kStringJetSdkToken);
final file = prerelease ? 'latest-prerelease.json' : 'latest.json';
return '$base/$kStringJetProjectId/bundles/flutter/$file?token=$enc';
}
/// Whole `{identifier}` tokens only — never replace `{user}` inside `{username}`.
final RegExp _arbPlaceholder = RegExp(r'\{([a-zA-Z_][a-zA-Z0-9_]*)\}');
String applyArbDemoPlaceholders(String raw) {
return raw.replaceAllMapped(_arbPlaceholder, (m) {
final name = m.group(1)!;
return kArbDemoPlaceholders[name] ?? m.group(0)!;
});
}
/// Fills placeholders in Flutter ARB-style templates (OTA or bundled).
String formatArbDemoLine(String key, String? raw) {
if (raw == null || raw.isEmpty) return '—';
if (key == 'hard_plural_apples') {
const count = 3;
try {
final j = jsonDecode(raw) as Map<String, dynamic>;
final branch = count == 1 ? 'one' : 'other';
final tpl = (j[branch] ?? j['other'] ?? raw).toString();
return applyArbDemoPlaceholders(tpl.replaceAll('{count}', '$count'));
} catch (_) {
return applyArbDemoPlaceholders(raw);
}
}
return applyArbDemoPlaceholders(raw);
}
Future<Map<String, String>> loadBundledArbEn() async {
try {
final text = await rootBundle.loadString(kBundledArbAsset);
final root = jsonDecode(text) as Map<String, dynamic>;
final out = <String, String>{};
for (final e in root.entries) {
final k = e.key;
if (k.startsWith('@@') || k.startsWith('@')) continue;
final v = e.value;
if (v is String) out[k] = v;
}
return out;
} catch (e, st) {
debugPrint('[StringJetFlutter] bundled ARB load failed: $e\n$st');
return {};
}
}
void _log(String msg) => debugPrint('[StringJetFlutter] $msg');
void main() {
runApp(const StringJetSampleApp());
}
class StringJetSampleApp extends StatelessWidget {
const StringJetSampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'StringJet Flutter sample',
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
List<String> _lines = [];
bool _loading = true;
StringJetNative? _native;
Map<String, String> _bundledEn = {};
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_load();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_log('App resumed — call _load() again if you add a refresh control; FFI cache is in native memory.');
}
}
Future<void> _load() async {
_log('--- load start ---');
_bundledEn = await loadBundledArbEn();
if (_bundledEn.isEmpty) {
_log('Bundled ARB empty or missing — add $kBundledArbAsset to pubspec assets.');
} else {
_log('Bundled ARB keys: ${_bundledEn.keys.join(", ")}');
}
_log('defaultTargetPlatform=$defaultTargetPlatform kIsWeb=$kIsWeb');
_log('Expected OTA URL (native side): ${expectedFlutterOtaUrl()}');
_log('Also watch Xcode/Console for Kotlin [StringJet] / StringJetOtaSdk lines when using FFI.');
final native = StringJetNative.open();
_native = null;
if (native == null) {
_log('FFI: library not loaded — no native OTA; showing bundled ARB only if present.');
setState(() {
_loading = false;
_lines = [
'FFI only: native library not loaded.',
'',
'Filter console for: StringJetFFI (Dart) and [StringJet] (Kotlin in worker).',
if (_bundledEn.isNotEmpty) ...[
'',
'Bundled import-fixtures (no FFI):',
...kFixtureKeys.map((k) => '$k → ${formatArbDemoLine(k, _bundledEn[k])}'),
] else ...[
'',
if (kIsWeb)
'Web has no dart:ffi; use the JS SDK sample.'
else if (defaultTargetPlatform == TargetPlatform.macOS)
'macOS: from repo run ./gradlew :otaSdk:copyStringjetSdkDylibFlutterMacSample (copies libstringjet_sdk.dylib into macos/Runner; Xcode embeds it under Contents/Frameworks).'
else if (defaultTargetPlatform == TargetPlatform.linux)
'Linux: ./gradlew :otaSdk:linkDebugSharedLinuxX64 then install libstringjet_sdk.so next to the executable.'
else if (defaultTargetPlatform == TargetPlatform.windows)
'Windows: link stringjet_sdk.dll next to the executable.'
else
'This target is not wired for desktop FFI; use Android/iOS SDK samples.',
],
];
});
_log('--- load end (no library) ---');
return;
}
final code = native.initEx(kStringJetSdkToken, kStringJetProjectId, fetchOnInit: 0, usePrerelease: 0);
_native = native;
if (code != 0) {
_log('stringjet_init_ex -> $code (setup may have failed).');
} else {
_log('init_ex OK (no OTA fetch). Calling syncTranslations() …');
}
final syncCode = native.syncTranslations();
if (syncCode != 0) {
_log('syncTranslations -> $syncCode (OTA may have failed). Using native get() where present, else bundled ARB.');
} else {
_log('syncTranslations OK. App-bar refresh calls syncTranslations() again.');
}
await Future<void>.delayed(const Duration(milliseconds: 500));
await _readKeysIntoLines(native);
_log('--- load end ---');
}
Future<void> _readKeysIntoLines(StringJetNative native) async {
const locale = 'en';
final out = <String>[];
for (final key in kFixtureKeys) {
final fromNative = native.get(key, locale);
final bundled = _bundledEn[key];
final raw = (fromNative != null && fromNative.isNotEmpty) ? fromNative : bundled;
if (fromNative == null || fromNative.isEmpty) {
if (bundled != null) {
_log('get("$key","$locale") miss or empty — bundled ARB fallback');
} else {
_log('get("$key","$locale") miss and no bundled string');
}
}
out.add('$key → ${formatArbDemoLine(key, raw)}');
}
setState(() {
_loading = false;
_lines = out;
});
}
Future<void> _onRefreshPressed() async {
final n = _native;
if (n == null) {
_log('Refresh: no native handle; running full _load()');
setState(() => _loading = true);
await _load();
return;
}
setState(() => _loading = true);
_log('Refresh: syncTranslations() …');
final r = n.syncTranslations();
_log('syncTranslations -> $r');
await Future<void>.delayed(const Duration(milliseconds: 200));
await _readKeysIntoLines(n);
_log('--- refresh end ---');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('import-fixtures (Flutter)'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'OTA: syncTranslations() then re-read keys',
onPressed: _onRefreshPressed,
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.all(24),
children: [
const Text(
'samples/import-fixtures',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 16),
..._lines.map((l) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(l),
)),
],
),
);
}
}