import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:ui' show Locale;
Locale? localeFromLanguageTag(String raw) {
final parts = raw.trim().replaceAll('_', '-').split('-');
if (parts.isEmpty || !RegExp(r'^[A-Za-z]{2,8}$').hasMatch(parts.first)) {
return null;
}
String? scriptCode;
String? countryCode;
for (final part in parts.skip(1)) {
if (part.length == 4 && RegExp(r'^[A-Za-z]{4}$').hasMatch(part)) {
scriptCode = part;
} else if ((part.length == 2 && RegExp(r'^[A-Za-z]{2}$').hasMatch(part)) ||
(part.length == 3 && RegExp(r'^\d{3}$').hasMatch(part))) {
countryCode = part;
}
}
return Locale.fromSubtags(
languageCode: parts.first,
scriptCode: scriptCode,
countryCode: countryCode,
);
}
String localeLanguageTag(Locale locale) => locale.toLanguageTag();
abstract interface class LocaleStore {
Future<String?> read();
Future<void> write(String languageCode);
}
class SharedPreferencesLocaleStore implements LocaleStore {
const SharedPreferencesLocaleStore();
static const _key = 'meinweimar.manual_locale';
@override
Future<String?> read() async =>
(await SharedPreferences.getInstance()).getString(_key);
@override
Future<void> write(String languageCode) async {
await (await SharedPreferences.getInstance()).setString(_key, languageCode);
}
}
class MemoryLocaleStore implements LocaleStore {
String? value;
@override
Future<String?> read() async => value;
@override
Future<void> write(String languageCode) async => value = languageCode;
}
class LocaleController extends ChangeNotifier {
LocaleController({LocaleStore? store})
: _store = store ?? const SharedPreferencesLocaleStore();
final LocaleStore _store;
Locale _locale = const Locale('de');
bool _hasManualSelection = false;
bool _onboardingComplete = false;
Locale get locale => _locale;
bool get hasManualSelection => _hasManualSelection;
bool get onboardingComplete => _onboardingComplete;
Future<void> load({Iterable<Locale> supported = const []}) async {
final stored = await _store.read();
final storedLocale = stored == null ? null : localeFromLanguageTag(stored);
final resolved = storedLocale == null
? null
: _bestSupported(storedLocale, supported);
if (resolved != null) {
_locale = resolved;
_hasManualSelection = true;
_onboardingComplete = true;
notifyListeners();
}
}
Future<void> select(Locale locale) async {
_locale = locale;
_hasManualSelection = true;
await _store.write(localeLanguageTag(locale));
notifyListeners();
}
Future<void> completeOnboarding() async {
_onboardingComplete = true;
notifyListeners();
}
void resolveFromSystem(
List<Locale> systemLocales, {
Iterable<Locale> supported = const [],
}) {
if (_hasManualSelection || systemLocales.isEmpty) return;
for (final systemLocale in systemLocales) {
final resolved = _bestSupported(systemLocale, supported);
if (resolved == null) continue;
_locale = resolved;
notifyListeners();
return;
}
}
Locale? _bestSupported(Locale requested, Iterable<Locale> supported) {
final available = supported.toList(growable: false);
if (available.isEmpty) return requested;
for (final candidate in available) {
if (_sameLocale(candidate, requested)) return candidate;
}
for (final candidate in available) {
if (candidate.languageCode.toLowerCase() ==
requested.languageCode.toLowerCase()) {
return candidate;
}
}
return null;
}
bool _sameLocale(Locale left, Locale right) {
return left.languageCode.toLowerCase() ==
right.languageCode.toLowerCase() &&
left.scriptCode?.toLowerCase() == right.scriptCode?.toLowerCase() &&
left.countryCode?.toUpperCase() == right.countryCode?.toUpperCase();
}
}
