163 lines
4.4 KiB
Dart
163 lines
4.4 KiB
Dart
import 'dart:ui' show Locale;
|
|
|
|
class RuntimeLocaleInfo {
|
|
const RuntimeLocaleInfo({
|
|
required this.requested,
|
|
required this.resolved,
|
|
required this.defaultLocale,
|
|
required this.supportedLocales,
|
|
required this.languageCode,
|
|
this.scriptCode,
|
|
this.countryCode,
|
|
});
|
|
|
|
final String requested;
|
|
final String resolved;
|
|
final String defaultLocale;
|
|
final List<String> supportedLocales;
|
|
final String languageCode;
|
|
final String? scriptCode;
|
|
final String? countryCode;
|
|
|
|
Map<String, Object?> toMap() {
|
|
return {
|
|
'requested': requested,
|
|
'resolved': resolved,
|
|
'default': defaultLocale,
|
|
'supported': supportedLocales,
|
|
'languageCode': languageCode,
|
|
if (scriptCode != null) 'scriptCode': scriptCode,
|
|
if (countryCode != null) 'countryCode': countryCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
class RuntimeLocaleResolver {
|
|
const RuntimeLocaleResolver._();
|
|
|
|
static RuntimeLocaleInfo resolve({
|
|
required Locale requested,
|
|
required String defaultLocale,
|
|
required List<String> supportedLocales,
|
|
}) {
|
|
final requestedTag = normalizeTag(tagOf(requested));
|
|
final fallback = normalizeTag(defaultLocale);
|
|
final supported = supportedLocales.isEmpty
|
|
? [fallback]
|
|
: supportedLocales.map(normalizeTag).toList(growable: false);
|
|
final resolved = _resolveTag(
|
|
requestedTag: requestedTag,
|
|
fallback: fallback,
|
|
supported: supported,
|
|
);
|
|
|
|
return RuntimeLocaleInfo(
|
|
requested: requestedTag,
|
|
resolved: resolved,
|
|
defaultLocale: fallback,
|
|
supportedLocales: supported,
|
|
languageCode: requested.languageCode,
|
|
scriptCode: requested.scriptCode,
|
|
countryCode: requested.countryCode,
|
|
);
|
|
}
|
|
|
|
static Locale localeFromTag(String tag) {
|
|
final parts = normalizeTag(tag).split('-');
|
|
if (parts.isEmpty || parts.first.isEmpty) {
|
|
throw const FormatException('Locale tag must not be empty');
|
|
}
|
|
|
|
String? scriptCode;
|
|
String? countryCode;
|
|
for (final part in parts.skip(1)) {
|
|
if (part.length == 4 && scriptCode == null) {
|
|
scriptCode = part;
|
|
} else {
|
|
countryCode ??= part;
|
|
}
|
|
}
|
|
|
|
return Locale.fromSubtags(
|
|
languageCode: parts.first,
|
|
scriptCode: scriptCode,
|
|
countryCode: countryCode,
|
|
);
|
|
}
|
|
|
|
static String tagOf(Locale locale) {
|
|
final parts = [locale.languageCode];
|
|
final scriptCode = locale.scriptCode;
|
|
final countryCode = locale.countryCode;
|
|
if (scriptCode != null && scriptCode.isNotEmpty) {
|
|
parts.add(scriptCode);
|
|
}
|
|
if (countryCode != null && countryCode.isNotEmpty) {
|
|
parts.add(countryCode);
|
|
}
|
|
return normalizeTag(parts.join('-'));
|
|
}
|
|
|
|
static String normalizeTag(String tag) {
|
|
final normalized = tag.trim().replaceAll('_', '-');
|
|
if (normalized.isEmpty) {
|
|
throw const FormatException('Locale tag must not be empty');
|
|
}
|
|
|
|
final parts = normalized
|
|
.split('-')
|
|
.where((part) => part.isNotEmpty)
|
|
.toList(growable: false);
|
|
if (parts.isEmpty) {
|
|
throw const FormatException('Locale tag must not be empty');
|
|
}
|
|
if (!_isLocalePart(parts.first)) {
|
|
throw FormatException('Locale language code is invalid: ${parts.first}');
|
|
}
|
|
|
|
final result = <String>[parts.first.toLowerCase()];
|
|
for (final part in parts.skip(1)) {
|
|
if (!_isLocalePart(part)) {
|
|
throw FormatException('Locale tag part is invalid: $part');
|
|
}
|
|
if (part.length == 4) {
|
|
result.add(
|
|
'${part[0].toUpperCase()}${part.substring(1).toLowerCase()}',
|
|
);
|
|
} else if (part.length == 2 || part.length == 3) {
|
|
result.add(part.toUpperCase());
|
|
} else {
|
|
result.add(part.toLowerCase());
|
|
}
|
|
}
|
|
return result.join('-');
|
|
}
|
|
|
|
static String _resolveTag({
|
|
required String requestedTag,
|
|
required String fallback,
|
|
required List<String> supported,
|
|
}) {
|
|
final supportedSet = supported.toSet();
|
|
if (supportedSet.contains(requestedTag)) {
|
|
return requestedTag;
|
|
}
|
|
|
|
final requestedLanguage = requestedTag.split('-').first;
|
|
for (final candidate in supported) {
|
|
if (candidate.split('-').first == requestedLanguage) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
if (supportedSet.contains(fallback)) {
|
|
return fallback;
|
|
}
|
|
return supported.first;
|
|
}
|
|
|
|
static bool _isLocalePart(String value) {
|
|
return RegExp(r'^[A-Za-z0-9]{2,8}$').hasMatch(value);
|
|
}
|
|
}
|