mirror of
https://github.com/chenasraf/i18n.git
synced 2026-05-17 17:38:07 +00:00
Initial commit
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.packages
|
||||
.idea
|
||||
*.iml
|
||||
# Remove the following pattern if you wish to check in your lock file
|
||||
pubspec.lock
|
||||
*.i18n.dart
|
||||
|
||||
# Conventional directory for build outputs
|
||||
build/
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
||||
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.8.5
|
||||
|
||||
- Initial fork from original library
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 fnx.io
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
263
README.md
Normal file
263
README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
Simple internationalization package for Dart and Flutter.
|
||||
|
||||
# Overview
|
||||
|
||||
Write your messages into YAML files, and let this package generate
|
||||
convenient Dart classes from those files.
|
||||
|
||||
Turn this **YAML** file:
|
||||
|
||||
lib/exampleMessages.i18n.yaml
|
||||
|
||||
button:
|
||||
save: Save
|
||||
load: Load
|
||||
users:
|
||||
welcome(String name): "Hello $name!"
|
||||
logout: Logout
|
||||
|
||||
Into these **generated** Dart classes:
|
||||
|
||||
class ExampleMessages {
|
||||
const ExampleMessages();
|
||||
ButtonExampleMessages get button => ButtonExampleMessages(this);
|
||||
UsersExampleMessages get users => UsersExampleMessages(this);
|
||||
}
|
||||
class ButtonExampleMessages {
|
||||
final ExampleMessages _parent;
|
||||
const ButtonExampleMessages(this._parent);
|
||||
String get save => "Save";
|
||||
String get load => "Load";
|
||||
}
|
||||
class UsersExampleMessages {
|
||||
final ExampleMessages _parent;
|
||||
const UsersExampleMessages(this._parent);
|
||||
String get logout => "Logout";
|
||||
String welcome(String name) => "Hello $name!";
|
||||
}
|
||||
|
||||
... and **use them** in your code - plain and simple.
|
||||
|
||||
ExampleMessages m = ExampleMessages();
|
||||
print(m.users.welcome('World'));
|
||||
// outputs: Hello World!
|
||||
|
||||
Package is an extension (custom builder) for [build_runner](https://pub.dartlang.org/packages/build_runner)
|
||||
(Dart standard for source generation) and it can be used with Flutter, AngularDart
|
||||
or any other type of Dart project.
|
||||
|
||||
# i18n: 51 points simpler than your standard i18n!
|
||||
|
||||
## Motivation and goals
|
||||
|
||||
* The official Dart/Flutter approach to i18n seems to be ... complicated and kind of ... heavyweight.
|
||||
* I would like my messages to be **checked during compile time**. Is that message really there?
|
||||
* Key to the localized message shouldn't be just some arbitrary String, it should be a **getter method**!
|
||||
* And if the message takes some **parameters**, the method should take those parameters!
|
||||
* I like to bundle messages into thematic groups, the i18n tool should support that and help me with it.
|
||||
* Dart has awesome **string interpolation**, I want to leverage that!
|
||||
* I like build_runner and code generation.
|
||||
* I love the name. i18n is hilarious.
|
||||
|
||||
## Solution
|
||||
|
||||
Write your messages into a YAML file:
|
||||
|
||||
exampleMessages.i18n.yaml (default messages):
|
||||
|
||||
generic:
|
||||
ok: OK
|
||||
done: DONE
|
||||
invoice:
|
||||
create: Create invoice
|
||||
delete: Delete invoice
|
||||
|
||||
|
||||
Write your translations into other YAML files:
|
||||
|
||||
exampleMessages_cs.i18n.yaml (_cs = Czech translation)
|
||||
|
||||
generic:
|
||||
done: Hotovo
|
||||
invoice:
|
||||
create: Vytvořit fakturu
|
||||
delete: Smazat fakturu
|
||||
|
||||
... run the `webdev` tool, or `build_runner` directly, and use your messages like this:
|
||||
|
||||
ExampleMessages m = ExampleMessages();
|
||||
print(m.generic.ok); // output: OK
|
||||
print(m.generic.done); // output: DONE
|
||||
|
||||
m = ExampleMessages_cs();
|
||||
print(m.generic.ok); // output: OK
|
||||
print(m.generic.done); // output: Hotovo
|
||||
|
||||
## Parameters and pluralization
|
||||
|
||||
The implementation is VERY straightforward, which allows you to do all sorts of crazy stuff:
|
||||
|
||||
invoice:
|
||||
create: Create invoice
|
||||
delete: Delete invoice
|
||||
help: "Use this function
|
||||
to generate new invoices and stuff.
|
||||
Awesome!"
|
||||
count(int cnt): "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}."
|
||||
apples:
|
||||
_apples(int cnt): "${_plural(cnt, one:'apple', many:'apples')}"
|
||||
count(int cnt): "You have eaten $cnt ${_apples(cnt)}."
|
||||
|
||||
Now see the generated classes:
|
||||
|
||||
class ExampleMessages {
|
||||
const ExampleMessages();
|
||||
InvoiceExampleMessages get invoice => InvoiceExampleMessages(this);
|
||||
ApplesExampleMessages get apples => ApplesExampleMessages(this);
|
||||
}
|
||||
|
||||
class InvoiceExampleMessages {
|
||||
final ExampleMessages _parent;
|
||||
const InvoiceExampleMessages(this._parent);
|
||||
String get create => "Create invoice";
|
||||
String get help => "Use this function to generate new invoices and stuff. Awesome!";
|
||||
String get delete => "Delete invoice";
|
||||
String count(int cnt) => "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}.";
|
||||
}
|
||||
|
||||
class ApplesExampleMessages {
|
||||
final ExampleMessages _parent;
|
||||
const ApplesExampleMessages(this._parent);
|
||||
String _apples(int cnt) => "${_plural(cnt, one:'apple', many:'apples')}";
|
||||
String count(int cnt) => "You have eaten $cnt ${_apples(cnt)}.";
|
||||
}
|
||||
|
||||
See how you can **reuse** the pluralization of `_apples(int cnt)`? (nice!)
|
||||
|
||||
There are three functions you can use in your message:
|
||||
|
||||
String _plural(int count, {String zero, String one, String two, String few, String many, String other})
|
||||
|
||||
String _cardinal(int count, {String zero, String one, String two, String few, String many, String other})
|
||||
|
||||
String _ordinal(int count, {String zero, String one, String two, String few, String many, String other})
|
||||
|
||||
`_plural` and `_cardinal` do the same. I just felt that `_plural`
|
||||
is a little bit less scary name :-)
|
||||
|
||||
We need only two forms of the word "apple" in English. "Apple" (one) and "apples" (many).
|
||||
But in Czech, we need three:
|
||||
|
||||
apples:
|
||||
_apples(int cnt): "${_plural(cnt, one:'jablko', few: 'jablka', many:'jablek')}"
|
||||
|
||||
See also:
|
||||
|
||||
* http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
* https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
||||
|
||||
## How to use generated classes
|
||||
|
||||
How to decide what translation to use (ExampleMessages_cs?, ExampleMessages_hu?) **is up to you**.
|
||||
The package simply generates message classes, that's all.
|
||||
|
||||
import 'exampleMessages.i18n.dart';
|
||||
import 'exampleMessages_cs.i18n.dart' deferred as cs;
|
||||
|
||||
void main() async {
|
||||
ExampleMessages m = ExampleMessages();
|
||||
print(m.apples.count(1));
|
||||
print(m.apples.count(2));
|
||||
print(m.apples.count(5));
|
||||
|
||||
await cs.loadLibrary();
|
||||
m = cs.ExampleMessages_cs(); // see? ExampleMessages_cs extends ExampleMessages
|
||||
print(m.apples.count(1));
|
||||
print(m.apples.count(2));
|
||||
print(m.apples.count(5));
|
||||
}
|
||||
|
||||
Where and how to store instances of these message classes -
|
||||
again, **up to you**. I would consider ScopedModel for Flutter and registering
|
||||
messages instance into dependency injection in AngularDart.
|
||||
|
||||
But in this case a singleton would be acceptable also.
|
||||
|
||||
## How to use with Flutter
|
||||
|
||||
Create YAML file with your messages, for example:
|
||||
|
||||
lib/messages/foo.i18n.yaml
|
||||
|
||||
Add `build_runner` as a dev_dependency and `i18n` as a dependency to `pubspec.yaml`:
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
i18n: any
|
||||
...
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: any
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Open a terminal and in the root of your Flutter project run:
|
||||
|
||||
flutter packages pub run build_runner watch
|
||||
|
||||
... and keep it running. Your message classes will appear next to YAML files and will be
|
||||
rebuilt automatically each time you change the source YAML.
|
||||
|
||||
For one-time (re)build of your messages run:
|
||||
|
||||
flutter packages pub run build_runner build
|
||||
|
||||
Import generated messages and use them:
|
||||
|
||||
import 'packages:my_app/messages/foo.i18n.dart'
|
||||
|
||||
...
|
||||
|
||||
Foo m = Foo();
|
||||
return Text(m.bar);
|
||||
...
|
||||
|
||||
## How to use with AngularDart
|
||||
|
||||
You are probably using `webdev` tool already, so you just need to add `i18n`
|
||||
as a dependency and **that's all**.
|
||||
|
||||
## Custom pluralization
|
||||
|
||||
The package can correctly decide between 'one', 'few', 'many', etc. only for
|
||||
English and Czech (for now). But you can easily plug your own language,
|
||||
see [example/main.dart](example/main.dart)
|
||||
and [Czech](lib/src/cs.dart) and [English](lib/src/en.dart)
|
||||
implementation.
|
||||
|
||||
If you implement support for your language, please let me know,
|
||||
I'll gladly embed it into the package.
|
||||
|
||||
# TODO
|
||||
|
||||
* More detailed docs.
|
||||
* Use it in some of our projects for real.
|
||||
* Current limitation: default language must be english
|
||||
* TODO: support custom imports
|
||||
|
||||
# Example
|
||||
|
||||
See [example](example). Clone the package repository ([https://github.com/fnx-io/i18n](https://github.com/fnx-io/i18n)) and run:
|
||||
|
||||
webdev serve example:8080
|
||||
|
||||
or
|
||||
|
||||
pub run build_runner serve example:8080
|
||||
|
||||
Now open the browser http://localhost:8080/ and watch the dev tools console.
|
||||
|
||||
# Credits
|
||||
|
||||
Created by [https://fnx.io](https://fnx.io).
|
||||
14
analysis_options.yaml
Normal file
14
analysis_options.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# Defines a default set of lint rules enforced for
|
||||
# projects at Google. For details and rationale,
|
||||
# see https://github.com/dart-lang/pedantic#enabled-lints.
|
||||
include: package:pedantic/analysis_options.yaml
|
||||
|
||||
# For lint rules and documentation, see http://dart-lang.github.io/linter/lints.
|
||||
# Uncomment to specify additional rules.
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
9
build.yaml
Normal file
9
build.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# Read about `build.yaml` at https://pub.dartlang.org/packages/build_config
|
||||
builders:
|
||||
|
||||
yamlBasedBuilder:
|
||||
import: "package:i18n/builder.dart"
|
||||
builder_factories: ["yamlBasedBuilder"]
|
||||
build_extensions: {".i69n.yaml": [".i18n.dart"]}
|
||||
build_to: source
|
||||
auto_apply: root_package
|
||||
13
example/exampleMessages.i18n.yaml
Normal file
13
example/exampleMessages.i18n.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
generic:
|
||||
ok: OK
|
||||
done: DONE
|
||||
invoice:
|
||||
create: Create invoice
|
||||
delete: Delete invoice
|
||||
help: "Use this function
|
||||
to generate new invoices and stuff.
|
||||
Awesome!"
|
||||
count(int cnt): "You have created $cnt ${_plural(cnt, one:'invoice', many:'invoices')}."
|
||||
apples:
|
||||
_apples(int cnt): "${_plural(cnt, one:'apple', many:'apples')}"
|
||||
count(int cnt): "You have eaten $cnt ${_apples(cnt)}."
|
||||
12
example/exampleMessages_cs.i18n.yaml
Normal file
12
example/exampleMessages_cs.i18n.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
generic:
|
||||
done: Hotovo
|
||||
invoice:
|
||||
create: Vytvořit fakturu
|
||||
delete: Smazat fakturu
|
||||
help: "Tuhle funkci použij
|
||||
na vytváření faktur.
|
||||
Boží!"
|
||||
count(int a): "Už jsi vytvořil ${_plural(a, one:'fakturu', few:'faktury', many:'faktur')}."
|
||||
apples:
|
||||
_apples(int cnt): "${_plural(cnt, one:'jablko', few: 'jablka', many:'jablek')}"
|
||||
count(int cnt): "Snědl jsi $cnt ${_apples(cnt)}."
|
||||
7
example/index.html
Normal file
7
example/index.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<p>Open the dev tools console ...</p>
|
||||
<script src="main.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
example/main.dart
Normal file
42
example/main.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
import 'package:i18n/i18n.dart' as i18n;
|
||||
|
||||
import 'exampleMessages.i18n.dart';
|
||||
import 'exampleMessages_cs.i18n.dart' deferred as cs;
|
||||
|
||||
void main() async {
|
||||
print("Hello from i18n!");
|
||||
print("Some english:");
|
||||
ExampleMessages m = ExampleMessages();
|
||||
print(m.generic.ok);
|
||||
print(m.generic.done);
|
||||
print(m.invoice.help);
|
||||
print(m.apples.count(1));
|
||||
print(m.apples.count(2));
|
||||
print(m.apples.count(5));
|
||||
|
||||
print("Asynchronous load of Czech messages:");
|
||||
await cs.loadLibrary();
|
||||
print("Some czech:");
|
||||
m = cs.ExampleMessages_cs();
|
||||
print(m.generic.ok); // inherited from default
|
||||
print(m.generic.done);
|
||||
print(m.invoice.help);
|
||||
print(m.apples.count(1));
|
||||
print(m.apples.count(2));
|
||||
print(m.apples.count(5));
|
||||
|
||||
// Override plurals for Czech or register support for your own language:
|
||||
i18n.registerResolver("cs", (int count, i18n.QuantityType type) {
|
||||
if (type == i18n.QuantityType.cardinal && count == 1) {
|
||||
return i18n.QuantityCategory.one;
|
||||
}
|
||||
return i18n.QuantityCategory.other;
|
||||
});
|
||||
|
||||
// See:
|
||||
// http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
// https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html
|
||||
}
|
||||
32
lib/builder.dart
Normal file
32
lib/builder.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:build/build.dart';
|
||||
import 'package:i18n/src/i18n_impl.dart';
|
||||
|
||||
Builder yamlBasedBuilder(BuilderOptions options) => YamlBasedBuilder();
|
||||
|
||||
class YamlBasedBuilder implements Builder {
|
||||
@override
|
||||
Future build(BuildStep buildStep) async {
|
||||
// Each [buildStep] has a single input.
|
||||
var inputId = buildStep.inputId;
|
||||
|
||||
// Create a new target [AssetId] based on the old one.
|
||||
var contents = await buildStep.readAsString(inputId);
|
||||
|
||||
var objectName = generateMessageObjectName(inputId.pathSegments.last);
|
||||
var dartContent = generateDartContentFromYaml(objectName, contents);
|
||||
|
||||
var copy = inputId.changeExtension(".dart");
|
||||
|
||||
// Write out the new asset.
|
||||
await buildStep.writeAsString(copy, dartContent);
|
||||
}
|
||||
|
||||
@override
|
||||
final buildExtensions = const {
|
||||
".i18n.yaml": [".i18n.dart"]
|
||||
};
|
||||
}
|
||||
99
lib/i18n.dart
Normal file
99
lib/i18n.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'src/cs.dart' as cs;
|
||||
import 'src/en.dart' as en;
|
||||
|
||||
///
|
||||
/// Language specific function, which is provided with a number and should return one of possible categories.
|
||||
/// count is never null.
|
||||
///
|
||||
typedef QuantityCategory CategoryResolver(int count, QuantityType type);
|
||||
|
||||
enum QuantityCategory { zero, one, two, few, many, other }
|
||||
|
||||
enum QuantityType { cardinal, ordinal }
|
||||
|
||||
void registerResolver(String languageCode, CategoryResolver resolver) {
|
||||
_resolverRegistry[languageCode] = resolver;
|
||||
}
|
||||
|
||||
///
|
||||
/// Same as ordinal.
|
||||
///
|
||||
String plural(int count, String languageCode, {String zero, String one, String two, String few, String many, String other}) {
|
||||
return _resolvePlural(count, languageCode, QuantityType.cardinal, zero: zero, one: one, two: two, few: few, many: many, other: other);
|
||||
}
|
||||
|
||||
///
|
||||
/// See: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
///
|
||||
String cardinal(int count, String languageCode, {String zero, String one, String two, String few, String many, String other}) {
|
||||
return _resolvePlural(count, languageCode, QuantityType.cardinal, zero: zero, one: one, two: two, few: few, many: many, other: other);
|
||||
}
|
||||
|
||||
///
|
||||
/// See: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
///
|
||||
String ordinal(int count, String languageCode, {String zero, String one, String two, String few, String many, String other}) {
|
||||
return _resolvePlural(count, languageCode, QuantityType.ordinal, zero: zero, one: one, two: two, few: few, many: many, other: other);
|
||||
}
|
||||
|
||||
Map<String, CategoryResolver> _resolverRegistry = {
|
||||
"en": en.quantityResolver,
|
||||
"cs": cs.quantityResolver,
|
||||
};
|
||||
|
||||
String _resolvePlural(int count, String languageCode, QuantityType type, {String zero, String one, String two, String few, String many, String other}) {
|
||||
QuantityCategory c = _resolveCategory(languageCode, count, type);
|
||||
if (c == null) c = QuantityCategory.other;
|
||||
if (many == null) many = other;
|
||||
switch (c) {
|
||||
case QuantityCategory.zero:
|
||||
return _firstNotNull(zero, many);
|
||||
case QuantityCategory.one:
|
||||
return _firstNotNull(one, many);
|
||||
case QuantityCategory.two:
|
||||
return _firstNotNull(two, many);
|
||||
case QuantityCategory.few:
|
||||
return _firstNotNull(few, many);
|
||||
case QuantityCategory.many:
|
||||
return _firstNotNull(many, other);
|
||||
case QuantityCategory.other:
|
||||
return _firstNotNull(other, many);
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
|
||||
QuantityCategory _defaultResolver(int count, QuantityType type) {
|
||||
switch (count) {
|
||||
case 0:
|
||||
return QuantityCategory.zero;
|
||||
case 1:
|
||||
return QuantityCategory.one;
|
||||
case 2:
|
||||
return QuantityCategory.two;
|
||||
case 3:
|
||||
return QuantityCategory.few;
|
||||
case 4:
|
||||
return QuantityCategory.few;
|
||||
}
|
||||
return QuantityCategory.other;
|
||||
}
|
||||
|
||||
QuantityCategory _resolveCategory(String languageCode, int count, QuantityType type) {
|
||||
if (count == null) return QuantityCategory.other;
|
||||
CategoryResolver resolver;
|
||||
if (languageCode != null) {
|
||||
resolver = _resolverRegistry[languageCode];
|
||||
if (resolver == null) {
|
||||
resolver = _defaultResolver;
|
||||
}
|
||||
} else {
|
||||
resolver = _defaultResolver;
|
||||
}
|
||||
return resolver(count, type);
|
||||
}
|
||||
|
||||
String _firstNotNull(String a, String b) {
|
||||
if (a != null) return a;
|
||||
if (b != null) return b;
|
||||
return "???";
|
||||
}
|
||||
32
lib/src/cs.dart
Normal file
32
lib/src/cs.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:i18n/i18n.dart';
|
||||
|
||||
///
|
||||
/// Quantity category resolver for czech.
|
||||
///
|
||||
/// See:
|
||||
///
|
||||
/// https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#cs
|
||||
///
|
||||
///
|
||||
QuantityCategory quantityResolver(int count, QuantityType type) {
|
||||
if (type == QuantityType.ordinal) return _resolveOrdinal(count);
|
||||
return _resolveCardinal(count);
|
||||
}
|
||||
|
||||
QuantityCategory _resolveCardinal(int count) {
|
||||
switch (count) {
|
||||
case 1:
|
||||
return QuantityCategory.one;
|
||||
case 2:
|
||||
return QuantityCategory.few;
|
||||
case 3:
|
||||
return QuantityCategory.few;
|
||||
case 4:
|
||||
return QuantityCategory.few;
|
||||
}
|
||||
return QuantityCategory.other;
|
||||
}
|
||||
|
||||
QuantityCategory _resolveOrdinal(int count) {
|
||||
return QuantityCategory.other;
|
||||
}
|
||||
31
lib/src/en.dart
Normal file
31
lib/src/en.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:i18n/i18n.dart';
|
||||
|
||||
///
|
||||
/// Quantity category resolver for english.
|
||||
///
|
||||
/// See:
|
||||
///
|
||||
/// https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#en
|
||||
///
|
||||
///
|
||||
QuantityCategory quantityResolver(int count, QuantityType type) {
|
||||
if (type == QuantityType.ordinal) return _resolveOrdinal(count);
|
||||
return _resolveCardinal(count);
|
||||
}
|
||||
|
||||
QuantityCategory _resolveCardinal(int count) {
|
||||
switch (count) {
|
||||
case 1:
|
||||
return QuantityCategory.one;
|
||||
}
|
||||
return QuantityCategory.other;
|
||||
}
|
||||
|
||||
QuantityCategory _resolveOrdinal(int count) {
|
||||
int mod10 = count % 10;
|
||||
int mod100 = count % 100;
|
||||
if (mod10 == 1 && mod100 != 11) return QuantityCategory.one;
|
||||
if (mod10 == 2 && mod100 != 12) return QuantityCategory.two;
|
||||
if (mod10 == 3 && mod100 != 13) return QuantityCategory.few;
|
||||
return QuantityCategory.other;
|
||||
}
|
||||
143
lib/src/i18n_impl.dart
Normal file
143
lib/src/i18n_impl.dart
Normal file
@@ -0,0 +1,143 @@
|
||||
library i18n;
|
||||
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
part 'model.dart';
|
||||
|
||||
Pattern twoCharsLower = RegExp("^[a-z]{2}\$");
|
||||
Pattern twoCharsUpper = RegExp("^[A-Z]{2}\$");
|
||||
|
||||
String generateDartContentFromYaml(ClassMeta meta, String yamlContent) {
|
||||
YamlMap messages = loadYaml(yamlContent);
|
||||
|
||||
List<TodoItem> todoList = [];
|
||||
|
||||
prepareTodoList(todoList, messages, meta);
|
||||
|
||||
StringBuffer output = StringBuffer();
|
||||
|
||||
output.writeln("// GENERATED FILE, do not edit!");
|
||||
output.writeln("import 'package:i18n/i18n.dart' as i18n;");
|
||||
if (meta.defaultFileName != null) {
|
||||
output.writeln("import '${meta.defaultFileName}';");
|
||||
}
|
||||
output.writeln("");
|
||||
output.writeln("String get _languageCode => '${meta.languageCode}';");
|
||||
output.writeln("String get _localeName => '${meta.localeName}';");
|
||||
output.writeln("");
|
||||
output.writeln("String _plural(int count, {String zero, String one, String two, String few, String many, String other}) =>");
|
||||
output.writeln("\ti18n.plural(count, _languageCode, zero:zero, one:one, two:two, few:few, many:many, other:other);");
|
||||
output.writeln("String _ordinal(int count, {String zero, String one, String two, String few, String many, String other}) =>");
|
||||
output.writeln("\ti18n.ordinal(count, _languageCode, zero:zero, one:one, two:two, few:few, many:many, other:other);");
|
||||
output.writeln("String _cardinal(int count, {String zero, String one, String two, String few, String many, String other}) =>");
|
||||
output.writeln("\ti18n.cardinal(count, _languageCode, zero:zero, one:one, two:two, few:few, many:many, other:other);");
|
||||
output.writeln("");
|
||||
|
||||
for (var todo in todoList) {
|
||||
renderTodoItem(todo, output);
|
||||
output.writeln("");
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
ClassMeta generateMessageObjectName(String fileName) {
|
||||
String name = fileName.replaceAll(".i18n.yaml", "");
|
||||
|
||||
List<String> nameParts = name.split("_");
|
||||
if (nameParts.isEmpty) {
|
||||
throw Exception(_renderFileNameError(name));
|
||||
}
|
||||
|
||||
ClassMeta result = ClassMeta();
|
||||
|
||||
result.defaultObjectName = _firstCharUpper(nameParts[0]);
|
||||
|
||||
if (nameParts.length == 1) {
|
||||
result.objectName = result.defaultObjectName;
|
||||
result.isDefault = true;
|
||||
result.languageCode = "en";
|
||||
result.localeName = "en";
|
||||
return result;
|
||||
} else {
|
||||
result.defaultFileName = "${nameParts[0]}.i18n.dart";
|
||||
result.isDefault = false;
|
||||
|
||||
if (nameParts.length > 3) {
|
||||
throw Exception(_renderFileNameError(name));
|
||||
}
|
||||
if (nameParts.length >= 2) {
|
||||
String languageCode = nameParts[1];
|
||||
if (twoCharsLower.allMatches(languageCode).length != 1) {
|
||||
throw Exception("Wrong language code '$languageCode' in file name '$fileName'. Language code must match $twoCharsLower");
|
||||
}
|
||||
result.languageCode = languageCode;
|
||||
result.localeName = languageCode;
|
||||
}
|
||||
if (nameParts.length == 3) {
|
||||
String countryCode = nameParts[2];
|
||||
if (twoCharsUpper.allMatches(countryCode).length != 1) {
|
||||
throw Exception("Wrong country code '$countryCode' in file name '$fileName'. Country code must match $twoCharsUpper");
|
||||
}
|
||||
result.localeName = "${result.languageCode}_${countryCode}";
|
||||
}
|
||||
result.objectName = "${result.defaultObjectName}_${result.localeName}";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
void renderTodoItem(TodoItem todo, StringBuffer output) {
|
||||
var meta = todo.meta;
|
||||
YamlMap content = todo.content;
|
||||
if (meta.isDefault) {
|
||||
output.writeln("class ${meta.objectName} {");
|
||||
} else {
|
||||
output.writeln("class ${meta.objectName} extends ${meta.defaultObjectName} {");
|
||||
}
|
||||
|
||||
if (meta.parent == null) {
|
||||
output.writeln("\tconst ${meta.objectName}();");
|
||||
} else {
|
||||
output.writeln("\tfinal ${meta.parent.objectName} _parent;");
|
||||
if (meta.isDefault) {
|
||||
output.writeln("\tconst ${meta.objectName}(this._parent);");
|
||||
} else {
|
||||
output.writeln("\tconst ${meta.objectName}(this._parent):super(_parent);");
|
||||
}
|
||||
}
|
||||
content.forEach((k, v) {
|
||||
if (v is YamlMap) {
|
||||
String prefix = _firstCharUpper(k);
|
||||
ClassMeta child = meta.nest(prefix);
|
||||
output.writeln("\t${child.objectName} get ${k} => ${child.objectName}(this);");
|
||||
} else {
|
||||
if (k.contains("(")) {
|
||||
// function
|
||||
output.writeln('\tString ${k} => "${v}";');
|
||||
} else {
|
||||
output.writeln('\tString get ${k} => "${v}";');
|
||||
}
|
||||
}
|
||||
});
|
||||
output.writeln("}");
|
||||
}
|
||||
|
||||
void prepareTodoList(List<TodoItem> todoList, YamlMap messages, ClassMeta name) {
|
||||
TodoItem todo = TodoItem(name, messages);
|
||||
todoList.add(todo);
|
||||
|
||||
messages.forEach((k, v) {
|
||||
if (v is YamlMap) {
|
||||
String prefix = _firstCharUpper(k);
|
||||
prepareTodoList(todoList, v, name.nest(prefix));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _firstCharUpper(String s) {
|
||||
return s.replaceRange(0, 1, s[0].toUpperCase());
|
||||
}
|
||||
|
||||
String _renderFileNameError(String name) {
|
||||
return "Wrong file name: '$name'";
|
||||
}
|
||||
29
lib/src/model.dart
Normal file
29
lib/src/model.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
part of i18n;
|
||||
|
||||
class ClassMeta {
|
||||
ClassMeta parent;
|
||||
bool isDefault;
|
||||
String defaultObjectName;
|
||||
String defaultFileName;
|
||||
String objectName;
|
||||
String localeName;
|
||||
String languageCode;
|
||||
|
||||
ClassMeta nest(String namePrefix) {
|
||||
ClassMeta result = ClassMeta();
|
||||
result.parent = this;
|
||||
result.isDefault = isDefault;
|
||||
result.defaultObjectName = "${namePrefix}${defaultObjectName}";
|
||||
result.objectName = "${namePrefix}${objectName}";
|
||||
result.localeName = localeName;
|
||||
result.languageCode = languageCode;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class TodoItem {
|
||||
ClassMeta meta;
|
||||
YamlMap content;
|
||||
|
||||
TodoItem(this.meta, this.content);
|
||||
}
|
||||
21
pubspec.yaml
Normal file
21
pubspec.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
name: i18n
|
||||
description: Simple internationalization tool for Dart and Flutter, based on YAML files and source generation.
|
||||
version: 0.8.5
|
||||
homepage: https://github.com/MohiuddinM/i18n
|
||||
author: Muhammad Mohiuddin <muhammad.mohiuddin@live.com>
|
||||
|
||||
environment:
|
||||
sdk: '>=2.1.0 <3.0.0'
|
||||
|
||||
dependencies:
|
||||
build: ^1.0.0
|
||||
# Not imported in code, but used to constrain `build.yaml` requirements
|
||||
build_config: ^0.3.0
|
||||
yaml: ^2.1.15
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.0.0
|
||||
test: ^1.0.0
|
||||
build_runner: ^1.0.0
|
||||
build_web_compilers: ^1.0.0
|
||||
|
||||
80
test/i18n_test.dart
Normal file
80
test/i18n_test.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:i18n/i18n.dart';
|
||||
import 'package:i18n/src/i18n_impl.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main() {
|
||||
group('Messages meta data', () {
|
||||
testMeta("messages", isDefault: true, defaultObjectName: "Messages", objectName: "Messages", languageCode: "en", localeName: "en");
|
||||
testMeta("messages_cs", isDefault: false, defaultObjectName: "Messages", objectName: "Messages_cs", languageCode: "cs", localeName: "cs");
|
||||
|
||||
testMeta("domainMessages", isDefault: true, defaultObjectName: "DomainMessages", objectName: "DomainMessages", languageCode: "en", localeName: "en");
|
||||
testMeta("domainMessages_cs", isDefault: false, defaultObjectName: "DomainMessages", objectName: "DomainMessages_cs", languageCode: "cs", localeName: "cs");
|
||||
testMeta("domainMessages_cs_CZ",
|
||||
isDefault: false, defaultObjectName: "DomainMessages", objectName: "DomainMessages_cs_CZ", languageCode: "cs", localeName: "cs_CZ");
|
||||
});
|
||||
|
||||
group('Plurals', () {
|
||||
test('en', () {
|
||||
expect(plural(1, "en", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("ONE!"));
|
||||
expect(plural(2, "en", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("OTHER!"));
|
||||
expect(plural(3, "en", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("OTHER!"));
|
||||
expect(plural(10, "en", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("OTHER!"));
|
||||
});
|
||||
|
||||
test('cs', () {
|
||||
expect(plural(1, "cs", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("ONE!"));
|
||||
expect(plural(2, "cs", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("FEW!"));
|
||||
expect(plural(3, "cs", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("FEW!"));
|
||||
expect(plural(10, "cs", one: "ONE!", few: "FEW!", other: "OTHER!"), equals("OTHER!"));
|
||||
});
|
||||
});
|
||||
|
||||
group('Message building', () {
|
||||
test('Todo list', () {
|
||||
ClassMeta root = new ClassMeta();
|
||||
root.objectName = "Test";
|
||||
root.defaultObjectName = "Test";
|
||||
List<TodoItem> todoList = [];
|
||||
var yaml = "foo:\n"
|
||||
" subfoo: subbar\n"
|
||||
" subfoo2: subbar2\n"
|
||||
"other: maybe\n"
|
||||
"or:\n"
|
||||
" status:\n"
|
||||
" name: not\n";
|
||||
|
||||
prepareTodoList(todoList, loadYaml(yaml), root);
|
||||
todoList.sort((a, b) {
|
||||
return a.meta.objectName.compareTo(b.meta.objectName);
|
||||
});
|
||||
expect(todoList.length, equals(4));
|
||||
expect(todoList[0].meta.objectName, equals("FooTest"));
|
||||
expect(todoList[1].meta.objectName, equals("OrTest"));
|
||||
expect(todoList[2].meta.objectName, equals("StatusOrTest"));
|
||||
expect(todoList[2].meta.parent, equals(todoList[1].meta));
|
||||
expect(todoList[2].meta.parent.parent, equals(todoList[3].meta));
|
||||
expect(todoList[3].meta.objectName, equals("Test"));
|
||||
expect(todoList[3].meta.parent, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void testMeta(String name, {bool isDefault, String defaultObjectName, String objectName, String languageCode, String localeName}) {
|
||||
ClassMeta meta = generateMessageObjectName(name);
|
||||
test('$name: isDefault', () {
|
||||
expect(meta.isDefault, equals(isDefault));
|
||||
});
|
||||
test('$name: defaultObjectName', () {
|
||||
expect(meta.defaultObjectName, equals(defaultObjectName));
|
||||
});
|
||||
test('$name: objectName', () {
|
||||
expect(meta.objectName, equals(objectName));
|
||||
});
|
||||
test('$name: localeName', () {
|
||||
expect(meta.localeName, equals(localeName));
|
||||
});
|
||||
test('$name: languageCode', () {
|
||||
expect(meta.languageCode, equals(languageCode));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user