mirror of
https://gitlab.com/TheOneWithTheBraid/dart_pkpass.git
synced 2025-07-05 12:58:47 +00:00
feat: add String localization support
Signed-off-by: The one with the braid <the-one@with-the-braid.cf>
This commit is contained in:
parent
78f88305ec
commit
e345763813
10 changed files with 173 additions and 13 deletions
26
README.md
26
README.md
|
@ -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.
|
- `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.
|
- `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
|
## Dependencies and compatibility
|
||||||
|
|
||||||
Any package should keep its dependencies as minimal as possible. Sometimes, there are specifications making this
|
Any package should keep its dependencies as minimal as possible. Sometimes, there are specifications making this
|
||||||
|
|
|
@ -25,6 +25,9 @@ Future<int> main(List<String> args) async {
|
||||||
print('First barcode: ${pass.metadata.barcodes.firstOrNull?.message}');
|
print('First barcode: ${pass.metadata.barcodes.firstOrNull?.message}');
|
||||||
print('Location: ${pass.metadata.locations.firstOrNull?.relevantText}');
|
print('Location: ${pass.metadata.locations.firstOrNull?.relevantText}');
|
||||||
print('Date: ${pass.metadata.relevantDate}');
|
print('Date: ${pass.metadata.relevantDate}');
|
||||||
|
print(
|
||||||
|
'Boarding pass: ${pass.metadata.boardingPass?.headerFields.firstOrNull?.getLocalizedLabel(pass, Locale.fromSubtags(languageCode: 'tlh'))}',
|
||||||
|
);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/models/barcode.dart';
|
||||||
import 'package:pkpass/src/utils/mabe_decode.dart';
|
import 'package:pkpass/src/utils/mabe_decode.dart';
|
||||||
import 'beacon.dart';
|
import 'beacon.dart';
|
||||||
|
@ -191,4 +194,22 @@ class PassMetadata {
|
||||||
webServiceURL: json['webServiceURL'] as String?,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import 'package:intl/locale.dart';
|
||||||
|
|
||||||
|
import 'package:pkpass/pkpass.dart';
|
||||||
import 'package:pkpass/src/utils/mabe_decode.dart';
|
import 'package:pkpass/src/utils/mabe_decode.dart';
|
||||||
|
|
||||||
/// Keys that define the structure of the pass.
|
/// Keys that define the structure of the pass.
|
||||||
|
@ -108,11 +111,26 @@ class DictionaryField {
|
||||||
? null
|
? null
|
||||||
: DictionaryValue.parse(json['attributedValue'] as String),
|
: 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 {
|
abstract class DictionaryValue {
|
||||||
const DictionaryValue();
|
const DictionaryValue();
|
||||||
|
|
||||||
|
/// parses the correct [DictionaryValue] implementor based on a given [value].
|
||||||
factory DictionaryValue.parse(String value) {
|
factory DictionaryValue.parse(String value) {
|
||||||
final number = int.tryParse(value);
|
final number = int.tryParse(value);
|
||||||
if (number != null) return NumberDictionaryValue(number);
|
if (number != null) return NumberDictionaryValue(number);
|
||||||
|
@ -122,35 +140,82 @@ abstract class DictionaryValue {
|
||||||
|
|
||||||
return StringDictionaryValue(value);
|
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 {
|
class StringDictionaryValue extends DictionaryValue {
|
||||||
final String string;
|
final String string;
|
||||||
|
|
||||||
const StringDictionaryValue(this.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 {
|
class DateTimeDictionaryValue extends DictionaryValue {
|
||||||
final DateTime dateTime;
|
final DateTime dateTime;
|
||||||
|
|
||||||
const DateTimeDictionaryValue(this.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 {
|
class NumberDictionaryValue extends DictionaryValue {
|
||||||
final int number;
|
final int number;
|
||||||
|
|
||||||
const NumberDictionaryValue(this.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 {
|
enum TransitType {
|
||||||
|
/// PKTransitTypeAir
|
||||||
air,
|
air,
|
||||||
|
|
||||||
|
/// PKTransitTypeBoat
|
||||||
boat,
|
boat,
|
||||||
|
|
||||||
|
/// PKTransitTypeBus
|
||||||
bus,
|
bus,
|
||||||
|
|
||||||
|
/// PKTransitTypeGeneric
|
||||||
generic,
|
generic,
|
||||||
|
|
||||||
|
/// PKTransitTypeTrain
|
||||||
train,
|
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 {
|
extension _TarnsitType on TransitType {
|
||||||
static TransitType? parse(String? type) {
|
static TransitType? parse(String? type) {
|
||||||
|
|
|
@ -16,6 +16,8 @@ class PassWebService {
|
||||||
required this.webServiceURL,
|
required this.webServiceURL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// returns a [PassWebService] in case [authenticationToken] and
|
||||||
|
/// [webServiceURL] are both valid values.
|
||||||
static PassWebService? maybe({
|
static PassWebService? maybe({
|
||||||
String? authenticationToken,
|
String? authenticationToken,
|
||||||
String? webServiceURL,
|
String? webServiceURL,
|
||||||
|
|
|
@ -6,13 +6,14 @@ import 'package:crypto/crypto.dart';
|
||||||
import 'package:intl/locale.dart';
|
import 'package:intl/locale.dart';
|
||||||
|
|
||||||
import 'package:pkpass/pkpass.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 _utf8codec = Utf8Codec();
|
||||||
final _jsonCodec = JsonCodec();
|
final _jsonCodec = JsonCodec();
|
||||||
|
|
||||||
class PassFile {
|
class PassFile {
|
||||||
PassFile(this.metadata, this._folder);
|
const PassFile(this.metadata, this._folder);
|
||||||
|
|
||||||
static Future<PassFile> parse(Uint8List pass) async {
|
static Future<PassFile> parse(Uint8List pass) async {
|
||||||
final codec = ZipDecoder();
|
final codec = ZipDecoder();
|
||||||
|
@ -87,6 +88,7 @@ class PassFile {
|
||||||
name: name,
|
name: name,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
|
extension: 'png',
|
||||||
);
|
);
|
||||||
if (path == null) return null;
|
if (path == null) return null;
|
||||||
final file = _folder.singleWhere((element) => element.name == path);
|
final file = _folder.singleWhere((element) => element.name == path);
|
||||||
|
@ -96,16 +98,35 @@ class PassFile {
|
||||||
|
|
||||||
Uint8List? getBackground({Locale? locale, int scale = 1}) =>
|
Uint8List? getBackground({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'background', locale: locale, scale: scale);
|
_matchUtf8List(name: 'background', locale: locale, scale: scale);
|
||||||
|
|
||||||
Uint8List? getFooter({Locale? locale, int scale = 1}) =>
|
Uint8List? getFooter({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'footer', locale: locale, scale: scale);
|
_matchUtf8List(name: 'footer', locale: locale, scale: scale);
|
||||||
|
|
||||||
Uint8List? getIcon({Locale? locale, int scale = 1}) =>
|
Uint8List? getIcon({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'icon', locale: locale, scale: scale);
|
_matchUtf8List(name: 'icon', locale: locale, scale: scale);
|
||||||
|
|
||||||
Uint8List? getLogo({Locale? locale, int scale = 1}) =>
|
Uint8List? getLogo({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'logo', locale: locale, scale: scale);
|
_matchUtf8List(name: 'logo', locale: locale, scale: scale);
|
||||||
|
|
||||||
Uint8List? getStrip({Locale? locale, int scale = 1}) =>
|
Uint8List? getStrip({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'strip', locale: locale, scale: scale);
|
_matchUtf8List(name: 'strip', locale: locale, scale: scale);
|
||||||
|
|
||||||
Uint8List? getThumbnail({Locale? locale, int scale = 1}) =>
|
Uint8List? getThumbnail({Locale? locale, int scale = 1}) =>
|
||||||
_matchUtf8List(name: 'thumbnail', locale: locale, scale: scale);
|
_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 {
|
extension on ArchiveFile {
|
||||||
|
|
|
@ -26,19 +26,31 @@ abstract class FileMatcher {
|
||||||
files = files.reversed.toList();
|
files = files.reversed.toList();
|
||||||
List<RegExp> expressions = <RegExp>[];
|
List<RegExp> expressions = <RegExp>[];
|
||||||
|
|
||||||
expressions.add(
|
// adding the fallbacks
|
||||||
RegExp(
|
// - match just *any* language
|
||||||
'^$name(@\\d+x)?\\.$extension\$',
|
// - match the five mostly spoken languages of the world, copied from Wikipedia
|
||||||
unicode: true,
|
expressions.addAll(
|
||||||
caseSensitive: false,
|
[
|
||||||
),
|
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) {
|
if (locale != null) {
|
||||||
final language = locale.languageCode;
|
final language = locale.languageCode;
|
||||||
expressions.add(
|
expressions.add(
|
||||||
RegExp(
|
RegExp(
|
||||||
'^$language(-[a-z]+)?\\.lproj/$name(@\\d+x)?\\.$extension\$',
|
'^$language(-[a-z]+)?\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
|
||||||
unicode: true,
|
unicode: true,
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
),
|
),
|
||||||
|
@ -48,7 +60,7 @@ abstract class FileMatcher {
|
||||||
if (region != null) {
|
if (region != null) {
|
||||||
expressions.add(
|
expressions.add(
|
||||||
RegExp(
|
RegExp(
|
||||||
'^$language-$region\\.lproj/$name(@\\d+x)?\\.$extension\$',
|
'^$language-$region\\.lproj\\/$name(@\\d+x)?\\.$extension\$',
|
||||||
unicode: true,
|
unicode: true,
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
),
|
),
|
10
lib/src/utils/lproj_parser.dart
Normal file
10
lib/src/utils/lproj_parser.dart
Normal 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)!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
name: pkpass
|
name: pkpass
|
||||||
description: A Dart native pkpass parsing library.
|
description: A Dart native pkpass parsing library.
|
||||||
version: 1.0.0
|
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
|
homepage: https://gitlab.com/TheOneWithTheBraid/dart_pkpass
|
||||||
issue_tracker: https://gitlab.com/TheOneWithTheBraid/dart_pkpass/-/issues
|
issue_tracker: https://gitlab.com/TheOneWithTheBraid/dart_pkpass/-/issues
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:intl/locale.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import 'package:pkpass/pkpass.dart';
|
import 'package:pkpass/pkpass.dart';
|
||||||
import 'package:pkpass/src/file_matcher.dart';
|
import 'package:pkpass/src/utils/file_matcher.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final archive = [
|
final archive = [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue