feat: add String localization support

Signed-off-by: The one with the braid <the-one@with-the-braid.cf>
This commit is contained in:
The one with the braid 2023-08-27 18:58:07 +02:00
parent 78f88305ec
commit e345763813
10 changed files with 173 additions and 13 deletions

View file

@ -1,3 +1,6 @@
import 'package:intl/locale.dart';
import 'package:pkpass/pkpass.dart';
import 'package:pkpass/src/models/barcode.dart';
import 'package:pkpass/src/utils/mabe_decode.dart';
import 'beacon.dart';
@ -191,4 +194,22 @@ class PassMetadata {
webServiceURL: json['webServiceURL'] as String?,
),
);
/// Localized version of [description] based on given [locale] and [pass].
String getLocalizedDescription(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return localizations?[description] ?? description;
}
/// Localized version of [organizationName] based on given [locale] and [pass].
String getLocalizedOrganizationName(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return localizations?[organizationName] ?? organizationName;
}
/// Localized version of [logoText] based on given [locale] and [pass].
String? getLocalizedLogoText(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return localizations?[logoText] ?? logoText;
}
}

View file

@ -1,3 +1,6 @@
import 'package:intl/locale.dart';
import 'package:pkpass/pkpass.dart';
import 'package:pkpass/src/utils/mabe_decode.dart';
/// Keys that define the structure of the pass.
@ -108,11 +111,26 @@ class DictionaryField {
? null
: DictionaryValue.parse(json['attributedValue'] as String),
);
/// Localized version of [label] based on given [locale] and [pass].
String? getLocalizedLabel(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
print(localizations);
return localizations?[label] ?? label;
}
/// Localized version of [changeMessage] based on given [locale] and [pass].
String? getLocalizedChangeMessage(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return localizations?[changeMessage] ?? changeMessage;
}
}
/// represents the possible values of a [DictionaryField].
abstract class DictionaryValue {
const DictionaryValue();
/// parses the correct [DictionaryValue] implementor based on a given [value].
factory DictionaryValue.parse(String value) {
final number = int.tryParse(value);
if (number != null) return NumberDictionaryValue(number);
@ -122,35 +140,82 @@ abstract class DictionaryValue {
return StringDictionaryValue(value);
}
/// Localized value based on given [locale] and [pass].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale);
}
/// [String] content of a [DictionaryField].
class StringDictionaryValue extends DictionaryValue {
final String string;
const StringDictionaryValue(this.string);
@override
/// Localized value based on given [locale] and [pass].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) {
final localizations = pass.getLocalizations(locale);
return StringDictionaryValue(localizations?[string] ?? string);
}
}
/// [DateTime] content of a [DictionaryField].
class DateTimeDictionaryValue extends DictionaryValue {
final DateTime dateTime;
const DateTimeDictionaryValue(this.dateTime);
@override
/// Localized value based on given [locale] and [pass]. Same as [dateTime].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) => this;
}
/// [int] content of a [DictionaryField].
class NumberDictionaryValue extends DictionaryValue {
final int number;
const NumberDictionaryValue(this.number);
@override
/// Localized value based on given [locale] and [pass]. Same as [number].
DictionaryValue getLocalizedValue(PassFile pass, Locale? locale) => this;
}
/// Possible types of [PassStructureDictionary.transitType].
enum TransitType {
/// PKTransitTypeAir
air,
/// PKTransitTypeBoat
boat,
/// PKTransitTypeBus
bus,
/// PKTransitTypeGeneric
generic,
/// PKTransitTypeTrain
train,
}
enum PassTextAlign { left, center, right, natural }
/// Possible types of [DictionaryField.textAlignment].
enum PassTextAlign {
/// PKTextAlignmentLeft, corresponds `dart:ui` [TextAlign.left]
left,
/// PKTextAlignmentCenter, corresponds `dart:ui` [TextAlign.center]
center,
/// PKTextAlignmentRight, corresponds `dart:ui` [TextAlign.left]
right,
/// PKTextAlignmentNatural, corresponds `dart:ui` [TextAlign.start]
natural,
}
extension _TarnsitType on TransitType {
static TransitType? parse(String? type) {

View file

@ -16,6 +16,8 @@ class PassWebService {
required this.webServiceURL,
});
/// returns a [PassWebService] in case [authenticationToken] and
/// [webServiceURL] are both valid values.
static PassWebService? maybe({
String? authenticationToken,
String? webServiceURL,

View file

@ -6,13 +6,14 @@ import 'package:crypto/crypto.dart';
import 'package:intl/locale.dart';
import 'package:pkpass/pkpass.dart';
import 'package:pkpass/src/file_matcher.dart';
import 'package:pkpass/src/utils/file_matcher.dart';
import 'package:pkpass/src/utils/lproj_parser.dart';
final _utf8codec = Utf8Codec();
final _jsonCodec = JsonCodec();
class PassFile {
PassFile(this.metadata, this._folder);
const PassFile(this.metadata, this._folder);
static Future<PassFile> parse(Uint8List pass) async {
final codec = ZipDecoder();
@ -87,6 +88,7 @@ class PassFile {
name: name,
scale: scale,
locale: locale,
extension: 'png',
);
if (path == null) return null;
final file = _folder.singleWhere((element) => element.name == path);
@ -96,16 +98,35 @@ class PassFile {
Uint8List? getBackground({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'background', locale: locale, scale: scale);
Uint8List? getFooter({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'footer', locale: locale, scale: scale);
Uint8List? getIcon({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'icon', locale: locale, scale: scale);
Uint8List? getLogo({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'logo', locale: locale, scale: scale);
Uint8List? getStrip({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'strip', locale: locale, scale: scale);
Uint8List? getThumbnail({Locale? locale, int scale = 1}) =>
_matchUtf8List(name: 'thumbnail', locale: locale, scale: scale);
Map<String, String>? getLocalizations(Locale? locale) {
final files = _folder.map((e) => e.name).toList();
final paths = FileMatcher.matchLocale(
files: files,
name: 'pass',
extension: 'strings',
locale: locale,
);
print(paths);
if (paths.isEmpty) return null;
final file = _folder.singleWhere((element) => element.name == paths.first);
return LProjParser.parse(file.stringContent);
}
}
extension on ArchiveFile {

View file

@ -26,19 +26,31 @@ abstract class FileMatcher {
files = files.reversed.toList();
List<RegExp> expressions = <RegExp>[];
expressions.add(
RegExp(
'^$name(@\\d+x)?\\.$extension\$',
unicode: true,
caseSensitive: false,
),
// adding the fallbacks
// - match just *any* language
// - match the five mostly spoken languages of the world, copied from Wikipedia
expressions.addAll(
[
RegExp(
'^([a-z]+(-[a-z]+)?\\.lproj\\/)?$name(@\\d+x)?\\.$extension\$',
unicode: true,
caseSensitive: false,
),
...['en', 'zh', 'hi', 'es', 'fr'].reversed.map(
(language) => RegExp(
'^$language(-[a-z]+)?\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
unicode: true,
caseSensitive: false,
),
),
],
);
if (locale != null) {
final language = locale.languageCode;
expressions.add(
RegExp(
'^$language(-[a-z]+)?\\.lproj/$name(@\\d+x)?\\.$extension\$',
'^$language(-[a-z]+)?\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
unicode: true,
caseSensitive: false,
),
@ -48,7 +60,7 @@ abstract class FileMatcher {
if (region != null) {
expressions.add(
RegExp(
'^$language-$region\\.lproj/$name(@\\d+x)?\\.$extension\$',
'^$language-$region\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
unicode: true,
caseSensitive: false,
),

View file

@ -0,0 +1,10 @@
abstract class LProjParser {
const LProjParser._();
static Map<String, String> parse(String stringsFile) => Map.fromEntries(
RegExp(r'"((?:\\"|[^"])*)"\s?=\s?"((?:\\"|[^"])*)"\s?;')
.allMatches(stringsFile)
.map(
(match) => MapEntry(match.group(1)!, match.group(2)!),
),
);
}