Skip to content

Commit

Permalink
feat(model): improve locales and translations handling (#273)
Browse files Browse the repository at this point in the history
* feat(model): improve to-string implementation of the base locale

* feat(helpers): added locale getter to translated name extension

* feat(cli): added l10n test generator

* refactor(cli): added generated tag for generated tests and excluded them in ci

* refactor(test): improve generated tests

* refactor(l10n): improve translations and l10n test generator

* feat(cli): print out translations count in l10n test generator

* feat(model): enhance basic locale with additional properties and methods

* feat(model): add utilities for converting different types of locales
  • Loading branch information
tsinis authored Jan 5, 2025
1 parent 52dd207 commit e3307e8
Show file tree
Hide file tree
Showing 18 changed files with 255 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/verify_package_workflow.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
- name: 🧪 Run Dart tests
if: ${{ !inputs.is_flutter }}
run: |
dart test --fail-fast --coverage=coverage
dart test test/src --fail-fast --coverage=coverage
dart run coverage:format_coverage -c -l -i coverage -o coverage/lcov.info --report-on=lib
- name: 🧪 Run Flutter tests
Expand Down
11 changes: 4 additions & 7 deletions .vscode/sealed_world.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,7 @@
"command": "dart pub get --no-example",
"label": "Dart Pub Get"
},
{
"type": "shell",
"command": "dart analyze .",
"label": "Dart Analyze"
},
{ "type": "shell", "command": "dart analyze .", "label": "Dart Analyze" },
{
"type": "shell",
"command": "dart format .",
Expand All @@ -258,10 +254,11 @@
"group": "build",
"label": "Dart Build Runner"
},
{ "type": "shell", "command": "dart test", "label": "Dart Test (All)" },
{
"type": "shell",
"command": "dart test --fail-fast --chain-stack-traces --coverage=coverage",
"label": "Dart Test"
"command": "dart test --exclude-tags generated --fail-fast --coverage=coverage",
"label": "Dart Test (Exclude Generated)"
},
{
"type": "shell",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ const zwlCurrencyTranslations = [
TranslatedName(LangVie(), name: "Đồng Đô la Zimbabwe"),
TranslatedName(LangZho(), name: "津巴布韦元"),
TranslatedName(LangZho(), name: "辛巴威元", script: ScriptHant()),
TranslatedName(LangInd(), name: "Dolar na Siombáibe"),
TranslatedName(LangMal(), name: "സിംബാബ്‌വേയൻ ഡോളർ"),
TranslatedName(LangAfr(), name: "Zimbabwiese dollar"),
TranslatedName(LangAmh(), name: "ዚምባብዌ ዶላር"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,28 @@ extension BasicLocaleExtension on BasicLocale {
countryCode: map["countryCode"]?.toString(),
script: Script.maybeFromCode(map["script"]),
);

/// Returns `true` if the locale has no country code specified.
bool get hasNoCountry => countryCode == null;

/// Returns `true` if the locale has no script specified.
bool get hasNoScript => script == null;

/// Returns `true` if the locale has only language specified.
bool get hasOnlyLanguage => hasNoScript && hasNoCountry;

/// Returns a string representing the locale.
///
/// This identifier should to be a valid Unicode Locale Identifier.
/// By default it uses Java-like underscore `_` [separator].
String toUnicodeLocaleId({String separator = "_"}) {
final languageCode = language.codeShort.toLowerCase();
if (hasOnlyLanguage) return languageCode;

final sb = StringBuffer(languageCode);
if (!hasNoScript) sb.write(separator + (script?.code ?? ""));
if (!hasNoCountry) sb.write(separator + (countryCode ?? ""));

return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ extension TranslatedExtension<T extends TranslatedName, L extends BasicLocale>
String? countryCode,
Script? script,
}) {
if (countryCode == null && script == null) return translations.first;
if (countryCode == null && script == null) {
return translations // TODO! Always put ones without code last.
.firstWhere((trn) => trn.countryCode == null && trn.script == null);
}
for (final l10n in translations) {
if (countryCode != null && l10n.countryCode != countryCode) continue;
if (script != null && l10n.script != script) continue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "../../model/core/basic_locale.dart";
import "../../model/language/language.dart";
import "../../model/script/writing_system.dart";
import "../../model/translated_name.dart";
Expand Down Expand Up @@ -64,4 +65,9 @@ extension TranslatedNameExtension on TranslatedName {
/// For example instead of `name.name` it's more natural/readable to use
/// `name.common`.
String get common => name;

/// Returns a [BasicLocale] object from this [TranslatedName] that represent
/// locale of translation.
BasicLocale get locale =>
BasicLocale(language, countryCode: countryCode, script: script);
}
30 changes: 18 additions & 12 deletions packages/sealed_languages/lib/src/model/core/basic_locale.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "../../helpers/extensions/basic_locale_extension.dart";
import "../../helpers/extensions/sealed_world_object_extension.dart";
import "../../interfaces/iso_standardized.dart";
import "../../interfaces/json_encodable.dart";
import "../language/language.dart";
Expand All @@ -10,32 +11,37 @@ class BasicLocale implements JsonEncodable<BasicLocale> {
///
/// The [language] parameter is required.
/// The [countryCode] and [script] parameters are optional.
const BasicLocale(this.language, {this.countryCode, this.script})
const BasicLocale(this.language, {String? countryCode, this.script})
: assert(
countryCode == null ||
countryCode.length >= IsoStandardized.codeShortLength,
"""`countryCode` have to be at least ${IsoStandardized.codeShortLength} characters long!""",
);
),
_countryCode = countryCode;

/// The [NaturalLanguage] representing the language of the locale.
final NaturalLanguage language;

/// The optional script information of type [Script].
final Script? script;

final String? _countryCode;

/// The region subtag for the locale.
///
/// This may be null, indicating that there is no specified region subtag.
/// This may be `null`, indicating that there is no specified region subtag.
///
/// This is expected to be string registered in the The two-letter ISO 3166-1
/// Alpha-2 code of the country.
final String? countryCode;

/// The optional script information of type [Script].
final Script? script;
/// This is expected to be UPPERCASE registered two-letter ISO 3166-1 Alpha-2
/// string code of the country.
String? get countryCode => _countryCode?.toUpperCaseIsoCode();

@override
String toJson({JsonCodec codec = const JsonCodec()}) => codec.encode(toMap());

@override
String toString() => "BasicLocale(${language.runtimeType}()"
'${countryCode == null ? '' : ', countryCode: "$countryCode"'}'
"${script == null ? '' : ', script: ${script.runtimeType}()'})";
String toString({bool short = true}) => short
? toUnicodeLocaleId()
: "BasicLocale(${language.runtimeType}()"
'${_countryCode == null ? '' : ', countryCode: "$countryCode"'}'
"${script == null ? '' : ', script: ${script.runtimeType}()'})";
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ const limLanguageTranslations = [
TranslatedName(LangSnd(), name: "لمبرگش"),
TranslatedName(LangSin(), name: "ලිම්බර්ගිශ්"),
TranslatedName(LangSqi(), name: "limburgisht"),
TranslatedName(LangSwa(), name: "Kilimburgi", countryCode: "CD"),
TranslatedName(LangSwa(), name: "Kilimburgi"),
TranslatedName(LangSwa(), name: "Kilimbugi", countryCode: "KE"),
TranslatedName(LangTir(), name: "ሊምበርግኛ"),
TranslatedName(LangTuk(), name: "limburg dili"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import "package:sealed_languages/src/data/scripts.data.dart";
import "package:sealed_languages/src/helpers/extensions/translated_name_extension.dart";
import "package:sealed_languages/src/model/core/basic_locale.dart";
import "package:sealed_languages/src/model/language/language.dart";
import "package:sealed_languages/src/model/script/writing_system.dart";
import "package:sealed_languages/src/model/translated_name.dart";
Expand All @@ -12,6 +14,21 @@ void main() => group("TranslatedNameExtension", () {
fullName: string,
);

test(
"locale",
() => expect(
value
.copyWith(script: const ScriptLatn(), countryCode: "SK")
.locale
.toString(),
BasicLocale(
NaturalLanguage.list.last,
script: const ScriptLatn(),
countryCode: "SK",
).toString(),
),
);

group("copyWith", () {
test("with non-null values", () {
final copy = value.copyWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ void main() => group("$BasicLocale", () {
expect(value.language, parsed.language);
expect(value.script, parsed.script);
});

test("toString", () => expect(value.toString(), "aa_Zzzz_01"));
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import "dart:ui" show Locale;

import "package:world_flags/world_flags.dart";

import "../models/locale/typed_locale.dart";
import "duration_extension.dart";

/// An extension on [TypedLocale] that provides utilities to manage translation
/// caches.
/// caches and transforms to other locale types.
extension TypedLocaleExtension<O extends Object, T extends TypedLocale<O>>
on T {
/// Returns typed locale in the more basic (parent) type [BasicLocale],
/// without translations and other additional properties.
BasicLocale get asBasicLocale =>
BasicLocale(language, countryCode: countryCode, script: script);

/// Returns a weak typed (String-based) SDK locale representation of [Locale].
Locale get asLocale => Locale.fromSubtags(
languageCode: languageCode,
countryCode: countryCode,
scriptCode: script?.code,
);

/// Synchronously returns a copy of this [TypedLocale] with updated
/// translation caches.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ base class TypedLocale<CountryType extends Object> extends Locale
this.countryTranslations = const {},
this.currencyTranslations = const {},
this.languageTranslations = const {},
}) : super(" ");
}) : super(" ", " ");

/// Creates an instance of [TypedLocale] from subtags.
///
Expand All @@ -60,7 +60,7 @@ base class TypedLocale<CountryType extends Object> extends Locale
}) : super.fromSubtags(
languageCode: language.codeShort.toLowerCase(),
scriptCode: script?.code,
countryCode: country?.toString().trim().toUpperCase(),
countryCode: country?.toUpperCaseIsoCode(),
);

/// Creates an instance of [TypedLocale] with implicit translations cache
Expand Down Expand Up @@ -137,6 +137,7 @@ base class TypedLocale<CountryType extends Object> extends Locale

@override
String toJson({JsonCodec codec = const JsonCodec()}) =>
BasicLocale(language, countryCode: countryCode, script: script)
.toJson(codec: codec);
asBasicLocale.toJson(codec: codec);

// TODO! Provide String toLanguageTag() => toUnicodeLocaleId('-'); override.
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "dart:ui";

import "package:_sealed_world_tests/sealed_world_tests.dart";
import "package:flutter_test/flutter_test.dart";
import "package:world_countries/src/extensions/typed_locale_extension.dart";
import "package:world_countries/src/models/locale/iso_locale.dart";
import "package:world_countries/src/models/locale/typed_locale.dart";
import "package:world_flags/world_flags.dart";
Expand Down Expand Up @@ -66,11 +67,18 @@ void main() => group("$TypedLocale", () {
test("toJson", () {
final json = value.toJson();
final parsed = json.parse(BasicLocaleExtension.fromMap);
expect(parsed.toString(), value.asBasicLocale.toString());
expect(value.countryCode, parsed.countryCode);
expect(value.language, parsed.language);
expect(value.script, parsed.script);
});

test("toString", () {
expect(value.toString(), locale.toString());
final fullValue = value.copyWith(script: const ScriptLatn());
expect(fullValue.toString(), fullValue.asLocale.toString());
});

group("copyWith", () {
test("with non-null values", () {
final copy = value.copyWith(
Expand Down
25 changes: 25 additions & 0 deletions tools/bin/l10n_tests.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// ignore_for_file: avoid_print, avoid-non-ascii-symbols
import "package:cli/constants/path_constants.dart";
import "package:cli/generators/l10n_test_generator.dart";
import "package:cli/utils/args_parser.dart";
import "package:cli/utils/io_utils.dart";

/// Usage: `dart run :l10n_tests sealed_countries`.
Future<void> main(List<String> args) {
final package = ArgsParser(args).maybePackageName();
if (package == null) throw ArgumentError("Package name should be provided.");

final dirName = package.dirName;
final path = join(
"../",
PathConstants.packages,
dirName,
PathConstants.test,
"generated",
"l10n",
);

final generator = L10NTestGenerator(package);

return generator.generate(path);
}
1 change: 1 addition & 0 deletions tools/lib/constants/path_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ sealed class PathConstants {
static const lib = "lib";
static const packages = "packages";
static const src = "src";
static const test = "test";
}
2 changes: 1 addition & 1 deletion tools/lib/generators/bool_getters_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class BoolGettersGenerator {
);
final currentImports = _code.readContentUntilFound(currentFilePath);
final buffer = StringBuffer(currentImports)..write("""
extension ${type}BoolGetters on $type {
extension ${type}BoolGetters on $type? {
""");

for (final item in package.dataList) {
Expand Down
Loading

0 comments on commit e3307e8

Please sign in to comment.