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

@ -21,6 +21,32 @@ Some parts of the PkPass specification are either not yet implemented, or not pl
- `nfc`: Card payment information for Apple Pay. - Not planned, feel free to contribute.
- `webService`: Information used to update passes using the web service. - Planned, feel free to contribute.
## Localizations
This package aims to implement PkPass localizations as well as possible. Any localizable value
can be accessed using a `getLocalized...` method, e.g. `myPass.getLocalizedDescription()` taking a `Locale`
as argument. In case the requested locale is not available, the following fallbacks are used:
- `en` - English, any
- `zh` - Chinese, generic language group
- `hi` - Hindi
- `es` - Spanish
- `fr` - French
- *In case neither available, take just any language you can find. We are likely dealing with a local product then.*
The used fallback languages are the five mostly understood languages in the world, feel free to propose better or more
precise fallback mechanisms.
## Barcode encodings
The PkPass standard is quite vague about the Barcode String encoding used. Technically, all IANA character set names
are allowed. Since this might be some overhead to implement, the following encoders are supported by default:
- `Latin1Codec` (default according to PkPass spec) - `iso-8859-1`, also fallback onto `iso-8859` and `iso8859`
- `Utf8Codec` (most common one) - `utf-8`, also fallback onto `utf8`
The supported encoders can be extended by adding a `String` `Encoder` pair to `PassBarcode.supportedCodecs`.
## Dependencies and compatibility
Any package should keep its dependencies as minimal as possible. Sometimes, there are specifications making this

View file

@ -25,6 +25,9 @@ Future<int> main(List<String> args) async {
print('First barcode: ${pass.metadata.barcodes.firstOrNull?.message}');
print('Location: ${pass.metadata.locations.firstOrNull?.relevantText}');
print('Date: ${pass.metadata.relevantDate}');
print(
'Boarding pass: ${pass.metadata.boardingPass?.headerFields.firstOrNull?.getLocalizedLabel(pass, Locale.fromSubtags(languageCode: 'tlh'))}',
);
return 0;
}

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)!),
),
);
}

View file

@ -1,7 +1,7 @@
name: pkpass
description: A Dart native pkpass parsing library.
version: 1.0.0
repository: https://gitlab.com/TheOneWithTheBraid/dart_pkpass
repository: https://gitlab.com/TheOneWithTheBraid/dart_pkpass.git
homepage: https://gitlab.com/TheOneWithTheBraid/dart_pkpass
issue_tracker: https://gitlab.com/TheOneWithTheBraid/dart_pkpass/-/issues

View file

@ -5,7 +5,7 @@ import 'package:intl/locale.dart';
import 'package:test/test.dart';
import 'package:pkpass/pkpass.dart';
import 'package:pkpass/src/file_matcher.dart';
import 'package:pkpass/src/utils/file_matcher.dart';
void main() {
final archive = [