feat: ship the interface in eight languages, with per-language plurals and mirroring
This commit is contained in:
parent
6de6672ae3
commit
a4a49f41d7
28 changed files with 1795 additions and 32 deletions
3
SPEC.md
3
SPEC.md
|
|
@ -73,6 +73,9 @@ The v1 client presents one reader screen:
|
||||||
- Each library page carries a search box that narrows it by title or address, and each row carries its own actions: remove a bookmark, leave a site or check it now, forget a page. History lists one row per address, headed by the most recent visit, rather than repeating a page once per visit. Clearing history takes a second step on a page the reader can simply leave, since it cannot be undone.
|
- Each library page carries a search box that narrows it by title or address, and each row carries its own actions: remove a bookmark, leave a site or check it now, forget a page. History lists one row per address, headed by the most recent visit, rather than repeating a page once per visit. Clearing history takes a second step on a page the reader can simply leave, since it cannot be undone.
|
||||||
- A change is a link too: following `mews:do/<verb>` applies it and rerenders the page in place without adding a history entry. Only a link followed from one of the client's own pages is honored, so a fetched page cannot clear history or change a setting on the reader's behalf (spec 8.1). A form on one of these pages is routed the same way rather than sent to the network.
|
- A change is a link too: following `mews:do/<verb>` applies it and rerenders the page in place without adding a history entry. Only a link followed from one of the client's own pages is honored, so a fetched page cannot clear history or change a setting on the reader's behalf (spec 8.1). A form on one of these pages is routed the same way rather than sent to the network.
|
||||||
- Our own pages are built from the same blocks as a page but are interface, not an article, so the renderer lays them out differently: wider before they stop growing, denser, rows divided by a line instead of bulleted, section headings quieter, a note under a row set smaller and dimmer, links carried by weight rather than an underline, and a one-field form on a single line.
|
- Our own pages are built from the same blocks as a page but are interface, not an article, so the renderer lays them out differently: wider before they stop growing, denser, rows divided by a line instead of bulleted, section headings quieter, a note under a row set smaller and dimmer, links carried by weight rather than an underline, and a one-field form on a single line.
|
||||||
|
- The interface ships in English, German, Spanish, French, Hebrew, Japanese, Portuguese and Russian. It follows the device's language by default and the reader can choose another on the settings page, where each language is named in its own words so it can be found from an interface they cannot read. A message a language has not translated falls back to English rather than disappearing.
|
||||||
|
- Hebrew mirrors the whole interface. Flutter's own widgets follow the same language, and our own pages take their direction from it; a fetched page keeps declaring its own, so an English interface still renders a Hebrew page right to left.
|
||||||
|
- Plurals are separate messages rather than a rule inside one, and the choice is made in code from the language's own rules: Russian separates two to four from the rest, Hebrew has a form for two, French and Portuguese count zero as singular, and Japanese has one form. A language needing a form English lacks names it with the same id and that suffix.
|
||||||
- Fonts are bundled, never fetched. The reader picks between two sans faces, Atkinson Hyperlegible and Luciole, and two serifs, Literata and Charis SIL; all four carry regular, italic, bold and bold italic, which the renderer needs for emphasis. The chosen face applies to the whole app, chrome included, so the address bar and the page below it are one face.
|
- Fonts are bundled, never fetched. The reader picks between two sans faces, Atkinson Hyperlegible and Luciole, and two serifs, Literata and Charis SIL; all four carry regular, italic, bold and bold italic, which the renderer needs for emphasis. The chosen face applies to the whole app, chrome included, so the address bar and the page below it are one face.
|
||||||
- A character the chosen face has no glyph for is looked for in that face's own fallbacks before the platform is asked. Hebrew always comes from a bundled Hebrew face matched to the shape of the chosen one, so it never depends on what the device has installed. The serifs also cover Greek, Cyrillic and a wide Latin range and fall back to each other; the two sans faces are narrow and have no bundled sans to turn to, so for those the platform answers, and it answers a sans with a sans. Scripts nothing bundled covers, such as Arabic and the CJK languages, always come from the platform.
|
- A character the chosen face has no glyph for is looked for in that face's own fallbacks before the platform is asked. Hebrew always comes from a bundled Hebrew face matched to the shape of the chosen one, so it never depends on what the device has installed. The serifs also cover Greek, Cyrillic and a wide Latin range and fall back to each other; the two sans faces are narrow and have no bundled sans to turn to, so for those the platform answers, and it answers a sans with a sans. Scripts nothing bundled covers, such as Arabic and the CJK languages, always come from the platform.
|
||||||
- Adding a font is four TTFs in `app/assets/fonts/` with its licence beside them, one `fonts` entry in `app/pubspec.yaml`, one entry in `app/lib/application/fonts.dart`, and one label in `app/locales/en-US/main.ftl`. A family named in the registry but missing from pubspec would render in the platform's font without saying so, so `app/test/fonts_test.dart` checks that the two agree.
|
- Adding a font is four TTFs in `app/assets/fonts/` with its licence beside them, one `fonts` entry in `app/pubspec.yaml`, one entry in `app/lib/application/fonts.dart`, and one label in `app/locales/en-US/main.ftl`. A family named in the registry but missing from pubspec would render in the platform's font without saying so, so `app/test/fonts_test.dart` checks that the two agree.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:signals/signals.dart';
|
import 'package:signals/signals.dart';
|
||||||
|
|
||||||
import '../../database/database.dart';
|
import '../../database/database.dart';
|
||||||
|
import '../../l10n/locales.dart';
|
||||||
import '../fonts.dart';
|
import '../fonts.dart';
|
||||||
|
|
||||||
/// The reader's presentation choices (spec 8.2) plus the per-app limits they
|
/// The reader's presentation choices (spec 8.2) plus the per-app limits they
|
||||||
|
|
@ -25,6 +26,10 @@ class ReaderSettingsController {
|
||||||
final themeMode = signal('system');
|
final themeMode = signal('system');
|
||||||
final variant = signal('site');
|
final variant = signal('site');
|
||||||
|
|
||||||
|
/// The interface language: a shipped locale tag, or [systemLanguage] to take
|
||||||
|
/// whatever the device asks for.
|
||||||
|
final language = signal(systemLanguage);
|
||||||
|
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final settings = await _db.readSettings();
|
final settings = await _db.readSettings();
|
||||||
if (settings == null) return;
|
if (settings == null) return;
|
||||||
|
|
@ -32,6 +37,7 @@ class ReaderSettingsController {
|
||||||
fontScale.value = settings.fontScale;
|
fontScale.value = settings.fontScale;
|
||||||
themeMode.value = settings.themeMode;
|
themeMode.value = settings.themeMode;
|
||||||
variant.value = settings.variant;
|
variant.value = settings.variant;
|
||||||
|
language.value = settings.language;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> setFont(String family) async {
|
Future<bool> setFont(String family) async {
|
||||||
|
|
@ -71,10 +77,19 @@ class ReaderSettingsController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> setLanguage(String tag) async {
|
||||||
|
if (tag != systemLanguage && matchLocale(tag) == null) return false;
|
||||||
|
if (tag == language.value) return false;
|
||||||
|
language.value = tag;
|
||||||
|
await _persist();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persist() => _db.saveSettings(
|
Future<void> _persist() => _db.saveSettings(
|
||||||
font: font.value,
|
font: font.value,
|
||||||
fontScale: fontScale.value,
|
fontScale: fontScale.value,
|
||||||
themeMode: themeMode.value,
|
themeMode: themeMode.value,
|
||||||
variant: variant.value,
|
variant: variant.value,
|
||||||
|
language: language.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,18 @@ import 'pages/mews_urls.dart';
|
||||||
class InternalActions {
|
class InternalActions {
|
||||||
InternalActions({
|
InternalActions({
|
||||||
required MewsDatabase db,
|
required MewsDatabase db,
|
||||||
|
required Future<void> Function() applyLanguage,
|
||||||
required ReaderSettingsController settings,
|
required ReaderSettingsController settings,
|
||||||
required BookmarksController bookmarks,
|
required BookmarksController bookmarks,
|
||||||
required SubscriptionsController subscriptions,
|
required SubscriptionsController subscriptions,
|
||||||
}) : _db = db,
|
}) : _db = db,
|
||||||
|
_applyLanguage = applyLanguage,
|
||||||
_settings = settings,
|
_settings = settings,
|
||||||
_bookmarks = bookmarks,
|
_bookmarks = bookmarks,
|
||||||
_subscriptions = subscriptions;
|
_subscriptions = subscriptions;
|
||||||
|
|
||||||
final MewsDatabase _db;
|
final MewsDatabase _db;
|
||||||
|
final Future<void> Function() _applyLanguage;
|
||||||
final ReaderSettingsController _settings;
|
final ReaderSettingsController _settings;
|
||||||
final BookmarksController _bookmarks;
|
final BookmarksController _bookmarks;
|
||||||
final SubscriptionsController _subscriptions;
|
final SubscriptionsController _subscriptions;
|
||||||
|
|
@ -37,6 +40,7 @@ class InternalActions {
|
||||||
'set-size' => _setSize(args['step']),
|
'set-size' => _setSize(args['step']),
|
||||||
'set-theme' => _settings.setThemeMode(args['value'] ?? ''),
|
'set-theme' => _settings.setThemeMode(args['value'] ?? ''),
|
||||||
'set-colors' => _settings.setVariant(args['value'] ?? ''),
|
'set-colors' => _settings.setVariant(args['value'] ?? ''),
|
||||||
|
'set-language' => _setLanguage(args['value']),
|
||||||
'mark-read' => _markRead(args, read: true),
|
'mark-read' => _markRead(args, read: true),
|
||||||
'mark-unread' => _markRead(args, read: false),
|
'mark-unread' => _markRead(args, read: false),
|
||||||
'refresh-subscriptions' => _refreshAll(),
|
'refresh-subscriptions' => _refreshAll(),
|
||||||
|
|
@ -52,6 +56,15 @@ class InternalActions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Changing the language has to load its messages before the page is built
|
||||||
|
/// again, or the rerender would still read the old ones.
|
||||||
|
Future<bool> _setLanguage(String? tag) async {
|
||||||
|
if (tag == null || tag.isEmpty) return false;
|
||||||
|
if (!await _settings.setLanguage(tag)) return false;
|
||||||
|
await _applyLanguage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _setSize(String? step) => switch (step) {
|
Future<bool> _setSize(String? step) => switch (step) {
|
||||||
'up' => _settings.increaseSize(),
|
'up' => _settings.increaseSize(),
|
||||||
'down' => _settings.decreaseSize(),
|
'down' => _settings.decreaseSize(),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ Future<MewsPage> buildBookmarksPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
textForm(
|
textForm(
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ Future<MewsPage> buildHistoryPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
textForm(
|
textForm(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ Future<MewsPage> buildHubPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: strings.t('hub-title'),
|
title: strings.t('hub-title'),
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
HeadingBlock(1, [MewsText(strings.t('hub-title'))]),
|
HeadingBlock(1, [MewsText(strings.t('hub-title'))]),
|
||||||
itemList([
|
itemList([
|
||||||
|
|
|
||||||
|
|
@ -196,25 +196,35 @@ ListBlock itemList(List<MewsListItem> items) =>
|
||||||
/// Wraps blocks as an internal document. The marker makes the renderer treat
|
/// Wraps blocks as an internal document. The marker makes the renderer treat
|
||||||
/// it as a Mews page rather than prepending the "not a Mews page" notice, and
|
/// it as a Mews page rather than prepending the "not a Mews page" notice, and
|
||||||
/// no entries are offered so an internal page is never subscribable.
|
/// no entries are offered so an internal page is never subscribable.
|
||||||
|
///
|
||||||
|
/// Unlike a fetched page, which declares its own language, one of ours is
|
||||||
|
/// written in whichever language the reader set, so it takes its direction
|
||||||
|
/// from that and mirrors when the interface does.
|
||||||
MewsPage internalPage({
|
MewsPage internalPage({
|
||||||
required Uri url,
|
required Uri url,
|
||||||
required String title,
|
required String title,
|
||||||
required List<MewsBlock> body,
|
required List<MewsBlock> body,
|
||||||
|
required Strings strings,
|
||||||
}) {
|
}) {
|
||||||
return MewsPage(
|
return MewsPage(
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
specVersion: 'internal',
|
specVersion: 'internal',
|
||||||
title: title,
|
title: title,
|
||||||
body: body,
|
body: body,
|
||||||
lang: 'en',
|
lang: strings.tag,
|
||||||
direction: TextDirection.ltr,
|
direction: strings.isRtl ? TextDirection.rtl : TextDirection.ltr,
|
||||||
variant: MewsVariant.plain,
|
variant: MewsVariant.plain,
|
||||||
entries: const [],
|
entries: const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Picks the message for [count]. Plurals are separate messages, so the
|
/// Picks the message for [count]. Plurals are separate messages rather than a
|
||||||
/// choice happens here and Fluent formats the number.
|
/// rule inside one, so the choice happens here.
|
||||||
|
///
|
||||||
|
/// A language needing a form English does not have, such as Russian's "few"
|
||||||
|
/// or Hebrew's "two", names it with the same id and that suffix. Those are
|
||||||
|
/// derived from [many] rather than passed at every call, and a language
|
||||||
|
/// without them falls through to [many].
|
||||||
String countText(
|
String countText(
|
||||||
Strings strings,
|
Strings strings,
|
||||||
int count, {
|
int count, {
|
||||||
|
|
@ -222,11 +232,15 @@ String countText(
|
||||||
required String one,
|
required String one,
|
||||||
required String many,
|
required String many,
|
||||||
}) {
|
}) {
|
||||||
return switch (count) {
|
if (count == 0) return strings.t(none);
|
||||||
0 => strings.t(none),
|
final suffix = strings.pluralSuffixFor(count);
|
||||||
1 => strings.t(one),
|
if (suffix == 'one') return strings.t(one, {'count': count});
|
||||||
_ => strings.t(many, {'count': count}),
|
if (suffix != 'many') {
|
||||||
};
|
final stem = many.substring(0, many.lastIndexOf('-'));
|
||||||
|
final text = strings.lookup('$stem-$suffix', {'count': count});
|
||||||
|
if (text != null) return text;
|
||||||
|
}
|
||||||
|
return strings.t(many, {'count': count});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YYYY-MM-DD, the shape a Mews entry link uses (spec 6.1).
|
/// YYYY-MM-DD, the shape a Mews entry link uses (spec 6.1).
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ Future<MewsPage> buildPostsPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
ParagraphBlock([
|
ParagraphBlock([
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import '../../l10n/strings.dart';
|
import '../../l10n/strings.dart';
|
||||||
|
import '../../l10n/locales.dart';
|
||||||
import '../../mews/document.dart';
|
import '../../mews/document.dart';
|
||||||
import '../fonts.dart';
|
import '../fonts.dart';
|
||||||
import '../controllers/reader_settings_controller.dart'
|
import '../controllers/reader_settings_controller.dart'
|
||||||
|
|
@ -33,6 +34,7 @@ Future<MewsPage> buildSettingsPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
HeadingBlock(2, [MewsText(strings.t('settings-font-heading'))]),
|
HeadingBlock(2, [MewsText(strings.t('settings-font-heading'))]),
|
||||||
|
|
@ -80,6 +82,22 @@ Future<MewsPage> buildSettingsPage(
|
||||||
current: mode == settings.themeMode.value,
|
current: mode == settings.themeMode.value,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
// The language a reader wants may not be the one they can currently
|
||||||
|
// read, so each is named in its own words.
|
||||||
|
HeadingBlock(2, [MewsText(strings.t('settings-language-heading'))]),
|
||||||
|
choiceRow([
|
||||||
|
(
|
||||||
|
href: MewsUrls.action('set-language', {'value': systemLanguage}),
|
||||||
|
label: strings.t('settings-language-system'),
|
||||||
|
current: settings.language.value == systemLanguage,
|
||||||
|
),
|
||||||
|
for (final locale in appLocales)
|
||||||
|
(
|
||||||
|
href: MewsUrls.action('set-language', {'value': locale.tag}),
|
||||||
|
label: strings.t(locale.labelId),
|
||||||
|
current: locale.tag == settings.language.value,
|
||||||
|
),
|
||||||
|
]),
|
||||||
HeadingBlock(2, [MewsText(strings.t('settings-variant-heading'))]),
|
HeadingBlock(2, [MewsText(strings.t('settings-variant-heading'))]),
|
||||||
choiceRow([
|
choiceRow([
|
||||||
for (final variant in ReaderSettingsController.variants)
|
for (final variant in ReaderSettingsController.variants)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ Future<MewsPage> buildSubscriptionsPage(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
textForm(
|
textForm(
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ Future<MewsPage> _index(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
...chrome(title: title, current: url, strings: strings),
|
...chrome(title: title, current: url, strings: strings),
|
||||||
if (counts.isEmpty)
|
if (counts.isEmpty)
|
||||||
|
|
@ -111,6 +112,7 @@ Future<MewsPage> _assign(
|
||||||
return internalPage(
|
return internalPage(
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
|
strings: strings,
|
||||||
body: [
|
body: [
|
||||||
HeadingBlock(1, [MewsText(title)]),
|
HeadingBlock(1, [MewsText(title)]),
|
||||||
ParagraphBlock([
|
ParagraphBlock([
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ class ReaderSettings extends Table {
|
||||||
RealColumn get fontScale => real().withDefault(const Constant(1.0))();
|
RealColumn get fontScale => real().withDefault(const Constant(1.0))();
|
||||||
TextColumn get themeMode => text().withDefault(const Constant('system'))();
|
TextColumn get themeMode => text().withDefault(const Constant('system'))();
|
||||||
TextColumn get variant => text().withDefault(const Constant('site'))();
|
TextColumn get variant => text().withDefault(const Constant('site'))();
|
||||||
|
TextColumn get language => text().withDefault(const Constant('system'))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
|
|
@ -116,13 +117,27 @@ class MewsDatabase extends _$MewsDatabase {
|
||||||
MewsDatabase.test(super.executor);
|
MewsDatabase.test(super.executor);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
/// Pre-release: the entry store replaces the old list of seen entry URLs and
|
|
||||||
/// the library tables gain real primary keys, so an existing database is
|
|
||||||
/// recreated rather than migrated.
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => destructiveFallback;
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
onCreate: (m) => m.createAll(),
|
||||||
|
onUpgrade: (m, from, to) async {
|
||||||
|
// Version 1 predates the entry store and the library tables' real
|
||||||
|
// primary keys. Nothing of it is worth carrying, so it is recreated;
|
||||||
|
// later versions only gain columns and keep what the reader has.
|
||||||
|
if (from < 2) {
|
||||||
|
for (final entity in allSchemaEntities.toList().reversed) {
|
||||||
|
await m.drop(entity);
|
||||||
|
}
|
||||||
|
await m.createAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (from < 3) {
|
||||||
|
await m.addColumn(readerSettings, readerSettings.language);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
static QueryExecutor _open() => LazyDatabase(() async {
|
static QueryExecutor _open() => LazyDatabase(() async {
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
|
@ -522,6 +537,7 @@ class MewsDatabase extends _$MewsDatabase {
|
||||||
required double fontScale,
|
required double fontScale,
|
||||||
required String themeMode,
|
required String themeMode,
|
||||||
required String variant,
|
required String variant,
|
||||||
|
required String language,
|
||||||
}) =>
|
}) =>
|
||||||
into(readerSettings).insert(
|
into(readerSettings).insert(
|
||||||
ReaderSettingsCompanion.insert(
|
ReaderSettingsCompanion.insert(
|
||||||
|
|
@ -530,6 +546,7 @@ class MewsDatabase extends _$MewsDatabase {
|
||||||
fontScale: Value(fontScale),
|
fontScale: Value(fontScale),
|
||||||
themeMode: Value(themeMode),
|
themeMode: Value(themeMode),
|
||||||
variant: Value(variant),
|
variant: Value(variant),
|
||||||
|
language: Value(language),
|
||||||
),
|
),
|
||||||
mode: InsertMode.insertOrReplace);
|
mode: InsertMode.insertOrReplace);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1711,6 +1711,18 @@ class $ReaderSettingsTable extends ReaderSettings
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
defaultValue: const Constant('site'),
|
defaultValue: const Constant('site'),
|
||||||
);
|
);
|
||||||
|
static const VerificationMeta _languageMeta = const VerificationMeta(
|
||||||
|
'language',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<String> language = GeneratedColumn<String>(
|
||||||
|
'language',
|
||||||
|
aliasedName,
|
||||||
|
false,
|
||||||
|
type: DriftSqlType.string,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: const Constant('system'),
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
id,
|
id,
|
||||||
|
|
@ -1718,6 +1730,7 @@ class $ReaderSettingsTable extends ReaderSettings
|
||||||
fontScale,
|
fontScale,
|
||||||
themeMode,
|
themeMode,
|
||||||
variant,
|
variant,
|
||||||
|
language,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
|
@ -1758,6 +1771,12 @@ class $ReaderSettingsTable extends ReaderSettings
|
||||||
variant.isAcceptableOrUnknown(data['variant']!, _variantMeta),
|
variant.isAcceptableOrUnknown(data['variant']!, _variantMeta),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('language')) {
|
||||||
|
context.handle(
|
||||||
|
_languageMeta,
|
||||||
|
language.isAcceptableOrUnknown(data['language']!, _languageMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1787,6 +1806,10 @@ class $ReaderSettingsTable extends ReaderSettings
|
||||||
DriftSqlType.string,
|
DriftSqlType.string,
|
||||||
data['${effectivePrefix}variant'],
|
data['${effectivePrefix}variant'],
|
||||||
)!,
|
)!,
|
||||||
|
language: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}language'],
|
||||||
|
)!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1802,12 +1825,14 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
final double fontScale;
|
final double fontScale;
|
||||||
final String themeMode;
|
final String themeMode;
|
||||||
final String variant;
|
final String variant;
|
||||||
|
final String language;
|
||||||
const ReaderSetting({
|
const ReaderSetting({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.font,
|
required this.font,
|
||||||
required this.fontScale,
|
required this.fontScale,
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
required this.variant,
|
required this.variant,
|
||||||
|
required this.language,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
|
|
@ -1817,6 +1842,7 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
map['font_scale'] = Variable<double>(fontScale);
|
map['font_scale'] = Variable<double>(fontScale);
|
||||||
map['theme_mode'] = Variable<String>(themeMode);
|
map['theme_mode'] = Variable<String>(themeMode);
|
||||||
map['variant'] = Variable<String>(variant);
|
map['variant'] = Variable<String>(variant);
|
||||||
|
map['language'] = Variable<String>(language);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1827,6 +1853,7 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
fontScale: Value(fontScale),
|
fontScale: Value(fontScale),
|
||||||
themeMode: Value(themeMode),
|
themeMode: Value(themeMode),
|
||||||
variant: Value(variant),
|
variant: Value(variant),
|
||||||
|
language: Value(language),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1841,6 +1868,7 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
fontScale: serializer.fromJson<double>(json['fontScale']),
|
fontScale: serializer.fromJson<double>(json['fontScale']),
|
||||||
themeMode: serializer.fromJson<String>(json['themeMode']),
|
themeMode: serializer.fromJson<String>(json['themeMode']),
|
||||||
variant: serializer.fromJson<String>(json['variant']),
|
variant: serializer.fromJson<String>(json['variant']),
|
||||||
|
language: serializer.fromJson<String>(json['language']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
|
|
@ -1852,6 +1880,7 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
'fontScale': serializer.toJson<double>(fontScale),
|
'fontScale': serializer.toJson<double>(fontScale),
|
||||||
'themeMode': serializer.toJson<String>(themeMode),
|
'themeMode': serializer.toJson<String>(themeMode),
|
||||||
'variant': serializer.toJson<String>(variant),
|
'variant': serializer.toJson<String>(variant),
|
||||||
|
'language': serializer.toJson<String>(language),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1861,12 +1890,14 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
double? fontScale,
|
double? fontScale,
|
||||||
String? themeMode,
|
String? themeMode,
|
||||||
String? variant,
|
String? variant,
|
||||||
|
String? language,
|
||||||
}) => ReaderSetting(
|
}) => ReaderSetting(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
font: font ?? this.font,
|
font: font ?? this.font,
|
||||||
fontScale: fontScale ?? this.fontScale,
|
fontScale: fontScale ?? this.fontScale,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
variant: variant ?? this.variant,
|
variant: variant ?? this.variant,
|
||||||
|
language: language ?? this.language,
|
||||||
);
|
);
|
||||||
ReaderSetting copyWithCompanion(ReaderSettingsCompanion data) {
|
ReaderSetting copyWithCompanion(ReaderSettingsCompanion data) {
|
||||||
return ReaderSetting(
|
return ReaderSetting(
|
||||||
|
|
@ -1875,6 +1906,7 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
fontScale: data.fontScale.present ? data.fontScale.value : this.fontScale,
|
fontScale: data.fontScale.present ? data.fontScale.value : this.fontScale,
|
||||||
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
|
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
|
||||||
variant: data.variant.present ? data.variant.value : this.variant,
|
variant: data.variant.present ? data.variant.value : this.variant,
|
||||||
|
language: data.language.present ? data.language.value : this.language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1885,13 +1917,15 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
..write('font: $font, ')
|
..write('font: $font, ')
|
||||||
..write('fontScale: $fontScale, ')
|
..write('fontScale: $fontScale, ')
|
||||||
..write('themeMode: $themeMode, ')
|
..write('themeMode: $themeMode, ')
|
||||||
..write('variant: $variant')
|
..write('variant: $variant, ')
|
||||||
|
..write('language: $language')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(id, font, fontScale, themeMode, variant);
|
int get hashCode =>
|
||||||
|
Object.hash(id, font, fontScale, themeMode, variant, language);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
|
|
@ -1900,7 +1934,8 @@ class ReaderSetting extends DataClass implements Insertable<ReaderSetting> {
|
||||||
other.font == this.font &&
|
other.font == this.font &&
|
||||||
other.fontScale == this.fontScale &&
|
other.fontScale == this.fontScale &&
|
||||||
other.themeMode == this.themeMode &&
|
other.themeMode == this.themeMode &&
|
||||||
other.variant == this.variant);
|
other.variant == this.variant &&
|
||||||
|
other.language == this.language);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
|
|
@ -1909,12 +1944,14 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
final Value<double> fontScale;
|
final Value<double> fontScale;
|
||||||
final Value<String> themeMode;
|
final Value<String> themeMode;
|
||||||
final Value<String> variant;
|
final Value<String> variant;
|
||||||
|
final Value<String> language;
|
||||||
const ReaderSettingsCompanion({
|
const ReaderSettingsCompanion({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.font = const Value.absent(),
|
this.font = const Value.absent(),
|
||||||
this.fontScale = const Value.absent(),
|
this.fontScale = const Value.absent(),
|
||||||
this.themeMode = const Value.absent(),
|
this.themeMode = const Value.absent(),
|
||||||
this.variant = const Value.absent(),
|
this.variant = const Value.absent(),
|
||||||
|
this.language = const Value.absent(),
|
||||||
});
|
});
|
||||||
ReaderSettingsCompanion.insert({
|
ReaderSettingsCompanion.insert({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
|
|
@ -1922,6 +1959,7 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
this.fontScale = const Value.absent(),
|
this.fontScale = const Value.absent(),
|
||||||
this.themeMode = const Value.absent(),
|
this.themeMode = const Value.absent(),
|
||||||
this.variant = const Value.absent(),
|
this.variant = const Value.absent(),
|
||||||
|
this.language = const Value.absent(),
|
||||||
});
|
});
|
||||||
static Insertable<ReaderSetting> custom({
|
static Insertable<ReaderSetting> custom({
|
||||||
Expression<int>? id,
|
Expression<int>? id,
|
||||||
|
|
@ -1929,6 +1967,7 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
Expression<double>? fontScale,
|
Expression<double>? fontScale,
|
||||||
Expression<String>? themeMode,
|
Expression<String>? themeMode,
|
||||||
Expression<String>? variant,
|
Expression<String>? variant,
|
||||||
|
Expression<String>? language,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
|
|
@ -1936,6 +1975,7 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
if (fontScale != null) 'font_scale': fontScale,
|
if (fontScale != null) 'font_scale': fontScale,
|
||||||
if (themeMode != null) 'theme_mode': themeMode,
|
if (themeMode != null) 'theme_mode': themeMode,
|
||||||
if (variant != null) 'variant': variant,
|
if (variant != null) 'variant': variant,
|
||||||
|
if (language != null) 'language': language,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1945,6 +1985,7 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
Value<double>? fontScale,
|
Value<double>? fontScale,
|
||||||
Value<String>? themeMode,
|
Value<String>? themeMode,
|
||||||
Value<String>? variant,
|
Value<String>? variant,
|
||||||
|
Value<String>? language,
|
||||||
}) {
|
}) {
|
||||||
return ReaderSettingsCompanion(
|
return ReaderSettingsCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -1952,6 +1993,7 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
fontScale: fontScale ?? this.fontScale,
|
fontScale: fontScale ?? this.fontScale,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
variant: variant ?? this.variant,
|
variant: variant ?? this.variant,
|
||||||
|
language: language ?? this.language,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1973,6 +2015,9 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
if (variant.present) {
|
if (variant.present) {
|
||||||
map['variant'] = Variable<String>(variant.value);
|
map['variant'] = Variable<String>(variant.value);
|
||||||
}
|
}
|
||||||
|
if (language.present) {
|
||||||
|
map['language'] = Variable<String>(language.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1983,7 +2028,8 @@ class ReaderSettingsCompanion extends UpdateCompanion<ReaderSetting> {
|
||||||
..write('font: $font, ')
|
..write('font: $font, ')
|
||||||
..write('fontScale: $fontScale, ')
|
..write('fontScale: $fontScale, ')
|
||||||
..write('themeMode: $themeMode, ')
|
..write('themeMode: $themeMode, ')
|
||||||
..write('variant: $variant')
|
..write('variant: $variant, ')
|
||||||
|
..write('language: $language')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
@ -3362,6 +3408,7 @@ typedef $$ReaderSettingsTableCreateCompanionBuilder =
|
||||||
Value<double> fontScale,
|
Value<double> fontScale,
|
||||||
Value<String> themeMode,
|
Value<String> themeMode,
|
||||||
Value<String> variant,
|
Value<String> variant,
|
||||||
|
Value<String> language,
|
||||||
});
|
});
|
||||||
typedef $$ReaderSettingsTableUpdateCompanionBuilder =
|
typedef $$ReaderSettingsTableUpdateCompanionBuilder =
|
||||||
ReaderSettingsCompanion Function({
|
ReaderSettingsCompanion Function({
|
||||||
|
|
@ -3370,6 +3417,7 @@ typedef $$ReaderSettingsTableUpdateCompanionBuilder =
|
||||||
Value<double> fontScale,
|
Value<double> fontScale,
|
||||||
Value<String> themeMode,
|
Value<String> themeMode,
|
||||||
Value<String> variant,
|
Value<String> variant,
|
||||||
|
Value<String> language,
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$ReaderSettingsTableFilterComposer
|
class $$ReaderSettingsTableFilterComposer
|
||||||
|
|
@ -3405,6 +3453,11 @@ class $$ReaderSettingsTableFilterComposer
|
||||||
column: $table.variant,
|
column: $table.variant,
|
||||||
builder: (column) => ColumnFilters(column),
|
builder: (column) => ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnFilters<String> get language => $composableBuilder(
|
||||||
|
column: $table.language,
|
||||||
|
builder: (column) => ColumnFilters(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$ReaderSettingsTableOrderingComposer
|
class $$ReaderSettingsTableOrderingComposer
|
||||||
|
|
@ -3440,6 +3493,11 @@ class $$ReaderSettingsTableOrderingComposer
|
||||||
column: $table.variant,
|
column: $table.variant,
|
||||||
builder: (column) => ColumnOrderings(column),
|
builder: (column) => ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ColumnOrderings<String> get language => $composableBuilder(
|
||||||
|
column: $table.language,
|
||||||
|
builder: (column) => ColumnOrderings(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$ReaderSettingsTableAnnotationComposer
|
class $$ReaderSettingsTableAnnotationComposer
|
||||||
|
|
@ -3465,6 +3523,9 @@ class $$ReaderSettingsTableAnnotationComposer
|
||||||
|
|
||||||
GeneratedColumn<String> get variant =>
|
GeneratedColumn<String> get variant =>
|
||||||
$composableBuilder(column: $table.variant, builder: (column) => column);
|
$composableBuilder(column: $table.variant, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumn<String> get language =>
|
||||||
|
$composableBuilder(column: $table.language, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$ReaderSettingsTableTableManager
|
class $$ReaderSettingsTableTableManager
|
||||||
|
|
@ -3505,12 +3566,14 @@ class $$ReaderSettingsTableTableManager
|
||||||
Value<double> fontScale = const Value.absent(),
|
Value<double> fontScale = const Value.absent(),
|
||||||
Value<String> themeMode = const Value.absent(),
|
Value<String> themeMode = const Value.absent(),
|
||||||
Value<String> variant = const Value.absent(),
|
Value<String> variant = const Value.absent(),
|
||||||
|
Value<String> language = const Value.absent(),
|
||||||
}) => ReaderSettingsCompanion(
|
}) => ReaderSettingsCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
font: font,
|
font: font,
|
||||||
fontScale: fontScale,
|
fontScale: fontScale,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
|
language: language,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
|
|
@ -3519,12 +3582,14 @@ class $$ReaderSettingsTableTableManager
|
||||||
Value<double> fontScale = const Value.absent(),
|
Value<double> fontScale = const Value.absent(),
|
||||||
Value<String> themeMode = const Value.absent(),
|
Value<String> themeMode = const Value.absent(),
|
||||||
Value<String> variant = const Value.absent(),
|
Value<String> variant = const Value.absent(),
|
||||||
|
Value<String> language = const Value.absent(),
|
||||||
}) => ReaderSettingsCompanion.insert(
|
}) => ReaderSettingsCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
font: font,
|
font: font,
|
||||||
fontScale: fontScale,
|
fontScale: fontScale,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
|
language: language,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
||||||
|
|
|
||||||
89
app/lib/l10n/locales.dart
Normal file
89
app/lib/l10n/locales.dart
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/// The interface languages the client ships. Pure data: the settings page
|
||||||
|
/// offers them, the loader reads their files, and the app tells Flutter which
|
||||||
|
/// one is in use.
|
||||||
|
library;
|
||||||
|
|
||||||
|
/// One shipped language: the tag used in settings and in the asset path, and
|
||||||
|
/// the id of its name in the locale files. A language is always named in its
|
||||||
|
/// own words, so a reader who cannot read the current interface can still find
|
||||||
|
/// theirs.
|
||||||
|
typedef AppLocale = ({String tag, String labelId});
|
||||||
|
|
||||||
|
/// Offered in this order, English first and then alphabetically by endonym.
|
||||||
|
const appLocales = <AppLocale>[
|
||||||
|
(tag: 'en-US', labelId: 'language-en'),
|
||||||
|
(tag: 'de', labelId: 'language-de'),
|
||||||
|
(tag: 'es', labelId: 'language-es'),
|
||||||
|
(tag: 'fr', labelId: 'language-fr'),
|
||||||
|
(tag: 'he', labelId: 'language-he'),
|
||||||
|
(tag: 'ja', labelId: 'language-ja'),
|
||||||
|
(tag: 'pt', labelId: 'language-pt'),
|
||||||
|
(tag: 'ru', labelId: 'language-ru'),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// What the reader's language setting holds when they have not chosen one.
|
||||||
|
const systemLanguage = 'system';
|
||||||
|
|
||||||
|
const fallbackLocale = 'en-US';
|
||||||
|
|
||||||
|
/// Written right to left, so the whole interface mirrors.
|
||||||
|
const rtlLocales = {'he'};
|
||||||
|
|
||||||
|
bool isRightToLeft(String tag) => rtlLocales.contains(_languageOf(tag));
|
||||||
|
|
||||||
|
/// The shipped locale for [tag], or null when nothing matches. A tag matches
|
||||||
|
/// on its language alone, so a device set to de-AT reads the German files.
|
||||||
|
String? matchLocale(String tag) {
|
||||||
|
final language = _languageOf(tag);
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
if (locale.tag == tag) return locale.tag;
|
||||||
|
}
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
if (_languageOf(locale.tag) == language) return locale.tag;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first of [tags] the client ships, for resolving the device's list of
|
||||||
|
/// preferred languages.
|
||||||
|
String resolveLocale(Iterable<String> tags) {
|
||||||
|
for (final tag in tags) {
|
||||||
|
final match = matchLocale(tag);
|
||||||
|
if (match != null) return match;
|
||||||
|
}
|
||||||
|
return fallbackLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _languageOf(String tag) =>
|
||||||
|
tag.split(RegExp('[-_]')).first.toLowerCase();
|
||||||
|
|
||||||
|
/// Which plural message a count needs, as a message id suffix. Zero is handled
|
||||||
|
/// before this, so every language can say "none" in its own way.
|
||||||
|
///
|
||||||
|
/// Only the languages the client ships are described (CLDR cardinal rules):
|
||||||
|
/// Russian separates two to four from the rest, Hebrew has a form for two,
|
||||||
|
/// French and Portuguese treat zero and one alike, and Japanese has one form.
|
||||||
|
String pluralSuffix(String tag, int count) {
|
||||||
|
final n = count.abs();
|
||||||
|
switch (_languageOf(tag)) {
|
||||||
|
case 'ru':
|
||||||
|
final mod10 = n % 10;
|
||||||
|
final mod100 = n % 100;
|
||||||
|
if (mod10 == 1 && mod100 != 11) return 'one';
|
||||||
|
if (mod10 >= 2 && mod10 <= 4 && !(mod100 >= 12 && mod100 <= 14)) {
|
||||||
|
return 'few';
|
||||||
|
}
|
||||||
|
return 'many';
|
||||||
|
case 'he':
|
||||||
|
if (n == 1) return 'one';
|
||||||
|
if (n == 2) return 'two';
|
||||||
|
return 'many';
|
||||||
|
case 'fr':
|
||||||
|
case 'pt':
|
||||||
|
return n <= 1 ? 'one' : 'many';
|
||||||
|
case 'ja':
|
||||||
|
return 'many';
|
||||||
|
default:
|
||||||
|
return n == 1 ? 'one' : 'many';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,27 +2,66 @@ import 'package:fluent/fluent.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'locales.dart';
|
||||||
|
|
||||||
/// Loads and formats the app's Fluent messages. All user-facing strings come
|
/// Loads and formats the app's Fluent messages. All user-facing strings come
|
||||||
/// from locales/en-US/main.ftl; lookups fall back to the message ID when a
|
/// from `locales/<tag>/main.ftl`.
|
||||||
/// message is missing so that missing translations are obvious in the UI.
|
///
|
||||||
|
/// A message missing from the chosen language falls back to English, and a
|
||||||
|
/// message missing from English too comes back as its own id, so a gap shows
|
||||||
|
/// in the interface rather than crashing or rendering as nothing.
|
||||||
class Strings {
|
class Strings {
|
||||||
final FluentBundle _bundle = FluentBundle('en-US');
|
FluentBundle _bundle = FluentBundle(fallbackLocale);
|
||||||
|
FluentBundle _fallback = FluentBundle(fallbackLocale);
|
||||||
|
|
||||||
/// Loads the messages from the app's asset bundle.
|
String _tag = fallbackLocale;
|
||||||
|
|
||||||
|
/// The language now in use, as a shipped locale tag.
|
||||||
|
String get tag => _tag;
|
||||||
|
|
||||||
|
bool get isRtl => isRightToLeft(_tag);
|
||||||
|
|
||||||
|
/// Loads English, which every other language falls back to.
|
||||||
Future<void> load() async {
|
Future<void> load() async {
|
||||||
final source = await rootBundle.loadString('locales/en-US/main.ftl');
|
_fallback = await _bundleFor(fallbackLocale);
|
||||||
loadSource(source);
|
_bundle = _fallback;
|
||||||
|
_tag = fallbackLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads messages from an already-read source, so tests can read the file
|
/// Switches to [tag], keeping English underneath it.
|
||||||
/// from disk instead of the asset bundle.
|
Future<void> use(String tag) async {
|
||||||
void loadSource(String source) {
|
if (tag == _tag) return;
|
||||||
_bundle.addMessages(source);
|
_bundle = tag == fallbackLocale ? _fallback : await _bundleFor(tag);
|
||||||
|
_tag = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
String t(String id, [Map<String, Object?> args = const {}]) {
|
Future<FluentBundle> _bundleFor(String tag) async {
|
||||||
return _bundle.format(id, args: args) ?? id;
|
final bundle = FluentBundle(tag);
|
||||||
|
bundle.addMessages(await rootBundle.loadString('locales/$tag/main.ftl'));
|
||||||
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads messages from an already-read source, so tests can read files from
|
||||||
|
/// disk instead of the asset bundle.
|
||||||
|
void loadSource(String source, {String tag = fallbackLocale}) {
|
||||||
|
final bundle = FluentBundle(tag);
|
||||||
|
bundle.addMessages(source);
|
||||||
|
_bundle = bundle;
|
||||||
|
_tag = tag;
|
||||||
|
if (tag == fallbackLocale) _fallback = bundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message for [id], or null when neither the chosen language nor
|
||||||
|
/// English has one. Used where a missing message is expected, such as a
|
||||||
|
/// plural form a language does not need.
|
||||||
|
String? lookup(String id, [Map<String, Object?> args = const {}]) =>
|
||||||
|
_bundle.format(id, args: args) ?? _fallback.format(id, args: args);
|
||||||
|
|
||||||
|
String t(String id, [Map<String, Object?> args = const {}]) =>
|
||||||
|
lookup(id, args) ?? id;
|
||||||
|
|
||||||
|
/// Which plural message [count] needs in the language in use.
|
||||||
|
String pluralSuffixFor(int count) => pluralSuffix(_tag, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provides [Strings] to the widget tree.
|
/// Provides [Strings] to the widget tree.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import 'dart:async' show unawaited;
|
import 'dart:async' show unawaited;
|
||||||
|
|
||||||
|
import 'dart:ui' show PlatformDispatcher;
|
||||||
|
|
||||||
import 'package:actors/actors.dart';
|
import 'package:actors/actors.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:signals/signals_flutter.dart' show SignalBuilder;
|
import 'package:signals/signals_flutter.dart' show SignalBuilder;
|
||||||
|
|
||||||
import '../application/actors/fetch_actor.dart';
|
import '../application/actors/fetch_actor.dart';
|
||||||
|
|
@ -13,6 +16,7 @@ import '../application/fonts.dart';
|
||||||
import '../application/internal_actions.dart';
|
import '../application/internal_actions.dart';
|
||||||
import '../application/internal_pages.dart';
|
import '../application/internal_pages.dart';
|
||||||
import '../database/database.dart';
|
import '../database/database.dart';
|
||||||
|
import '../l10n/locales.dart';
|
||||||
import '../l10n/strings.dart';
|
import '../l10n/strings.dart';
|
||||||
import '../mews/document.dart';
|
import '../mews/document.dart';
|
||||||
import 'reader/palettes.dart';
|
import 'reader/palettes.dart';
|
||||||
|
|
@ -38,6 +42,7 @@ class AppDependencies {
|
||||||
strings: this.strings);
|
strings: this.strings);
|
||||||
internalActions = InternalActions(
|
internalActions = InternalActions(
|
||||||
db: db,
|
db: db,
|
||||||
|
applyLanguage: applyLanguage,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
bookmarks: bookmarks,
|
bookmarks: bookmarks,
|
||||||
subscriptions: subscriptions,
|
subscriptions: subscriptions,
|
||||||
|
|
@ -65,8 +70,20 @@ class AppDependencies {
|
||||||
late final InternalPages internalPages;
|
late final InternalPages internalPages;
|
||||||
late final InternalActions internalActions;
|
late final InternalActions internalActions;
|
||||||
|
|
||||||
|
/// The language to read the interface in: the reader's choice, or the best
|
||||||
|
/// match for what the device asks for.
|
||||||
|
String get activeLocale => settings.language.value == systemLanguage
|
||||||
|
? resolveLocale(
|
||||||
|
PlatformDispatcher.instance.locales.map((l) => l.toLanguageTag()))
|
||||||
|
: settings.language.value;
|
||||||
|
|
||||||
|
/// Loads the chosen language's messages. Called at startup and whenever the
|
||||||
|
/// reader changes the setting.
|
||||||
|
Future<void> applyLanguage() => strings.use(activeLocale);
|
||||||
|
|
||||||
Future<void> start() async {
|
Future<void> start() async {
|
||||||
await settings.load();
|
await settings.load();
|
||||||
|
await applyLanguage();
|
||||||
await bookmarks.load();
|
await bookmarks.load();
|
||||||
await subscriptions.load();
|
await subscriptions.load();
|
||||||
await browser.restoreLastSession();
|
await browser.restoreLastSession();
|
||||||
|
|
@ -129,13 +146,30 @@ class _ThemedApp extends StatelessWidget {
|
||||||
};
|
};
|
||||||
final settings = deps.settings;
|
final settings = deps.settings;
|
||||||
final font = settings.font.value;
|
final font = settings.font.value;
|
||||||
|
// Read so the whole app rebuilds when the reader changes language.
|
||||||
|
settings.language.value;
|
||||||
final page = deps.browser.currentPage.value;
|
final page = deps.browser.currentPage.value;
|
||||||
final variant = settings.variant.value == 'site'
|
final variant = settings.variant.value == 'site'
|
||||||
? (page?.variant ?? MewsVariant.plain)
|
? (page?.variant ?? MewsVariant.plain)
|
||||||
: pageVariantFromName(settings.variant.value);
|
: pageVariantFromName(settings.variant.value);
|
||||||
|
|
||||||
|
// Flutter's own widgets and the text direction follow the same
|
||||||
|
// language, so a right-to-left interface mirrors without us asking.
|
||||||
|
final locale = deps.activeLocale;
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Mews',
|
title: 'Mews',
|
||||||
|
locale: Locale(locale.split('-').first,
|
||||||
|
locale.contains('-') ? locale.split('-').last : null),
|
||||||
|
supportedLocales: [
|
||||||
|
for (final l in appLocales)
|
||||||
|
Locale(l.tag.split('-').first,
|
||||||
|
l.tag.contains('-') ? l.tag.split('-').last : null),
|
||||||
|
],
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
themeMode: mode,
|
themeMode: mode,
|
||||||
theme: _themeFor(
|
theme: _themeFor(
|
||||||
paletteFor(pageVariant: variant, readerVariant: 'site', dark: false),
|
paletteFor(pageVariant: variant, readerVariant: 'site', dark: false),
|
||||||
|
|
|
||||||
172
app/locales/de/main.ftl
Normal file
172
app/locales/de/main.ftl
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = Webadresse eingeben
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = Webadresse
|
||||||
|
reader-reload = Diese Seite neu laden
|
||||||
|
reader-open-hub = Deine Mews-Seiten öffnen
|
||||||
|
reader-back = Zurück
|
||||||
|
reader-forward = Vorwärts
|
||||||
|
reader-bookmark-add = Lesezeichen hinzufügen
|
||||||
|
reader-bookmark-remove = Lesezeichen entfernen
|
||||||
|
reader-subscribe = Diese Website abonnieren
|
||||||
|
reader-unsubscribe = Abo beenden
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = Beiträge
|
||||||
|
nav-bookmarks = Lesezeichen
|
||||||
|
nav-subscriptions = Abos
|
||||||
|
nav-history = Verlauf
|
||||||
|
nav-tags = Schlagwörter
|
||||||
|
nav-settings = Einstellungen
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = Noch keine Lesezeichen
|
||||||
|
count-bookmarks-one = 1 Lesezeichen
|
||||||
|
count-bookmarks-many = { $count } Lesezeichen
|
||||||
|
count-sites-none = Noch keine Websites
|
||||||
|
count-sites-one = 1 Website
|
||||||
|
count-sites-many = { $count } Websites
|
||||||
|
count-pages-none = Noch keine Seiten
|
||||||
|
count-pages-one = 1 Seite
|
||||||
|
count-pages-many = { $count } Seiten
|
||||||
|
count-tags-none = Noch keine Schlagwörter
|
||||||
|
count-tags-one = 1 Schlagwort
|
||||||
|
count-tags-many = { $count } Schlagwörter
|
||||||
|
count-unread-none = Keine ungelesenen Beiträge
|
||||||
|
count-unread-one = 1 ungelesener Beitrag
|
||||||
|
count-unread-many = { $count } ungelesene Beiträge
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = Beiträge
|
||||||
|
posts-empty = Hier erscheinen Beiträge der Websites, die du abonnierst.
|
||||||
|
posts-none-unread = Du hast alles gelesen.
|
||||||
|
posts-checking = Es wird nach neuen Beiträgen gesucht.
|
||||||
|
posts-sort-label = Sortieren nach
|
||||||
|
posts-sort-date = Datum
|
||||||
|
posts-sort-source = Website
|
||||||
|
posts-sort-unread = Ungelesene zuerst
|
||||||
|
posts-show-label = Anzeigen
|
||||||
|
posts-show-all = Alle Beiträge
|
||||||
|
posts-show-unread = Nur ungelesene
|
||||||
|
posts-mark-read = Als gelesen markieren
|
||||||
|
posts-mark-unread = Als ungelesen markieren
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = Nach neuen Beiträgen suchen
|
||||||
|
action-search = Suchen
|
||||||
|
action-clear-search = Suche zurücksetzen
|
||||||
|
action-remove-bookmark = Lesezeichen entfernen
|
||||||
|
action-unsubscribe = Abo beenden
|
||||||
|
action-edit-tags = Schlagwörter bearbeiten
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = Verlauf
|
||||||
|
history-empty = Hier erscheinen die Seiten, die du besuchst.
|
||||||
|
history-no-matches = Keine Seiten passen zu deiner Suche.
|
||||||
|
history-search-label = Verlauf durchsuchen
|
||||||
|
history-forget = Aus dem Verlauf entfernen
|
||||||
|
history-clear = Verlauf löschen
|
||||||
|
history-clear-confirm = Beim Löschen des Verlaufs werden alle besuchten Seiten entfernt. Das lässt sich nicht rückgängig machen.
|
||||||
|
history-clear-yes = Verlauf jetzt löschen
|
||||||
|
history-clear-no = Verlauf behalten
|
||||||
|
bookmarks-title = Lesezeichen
|
||||||
|
bookmarks-empty = Hier erscheinen die Seiten, die du als Lesezeichen speicherst.
|
||||||
|
bookmarks-no-matches = Keine Lesezeichen passen zu deiner Suche.
|
||||||
|
bookmarks-search-label = Lesezeichen durchsuchen
|
||||||
|
subscriptions-title = Abos
|
||||||
|
subscriptions-empty = Hier erscheinen die Websites, die du abonnierst.
|
||||||
|
subscriptions-no-matches = Keine Websites passen zu deiner Suche.
|
||||||
|
subscriptions-search-label = Websites durchsuchen
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = Zuletzt geprüft { $date }
|
||||||
|
subscriptions-never-checked = Noch nicht geprüft
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = Schlagwörter
|
||||||
|
tags-empty = Hier erscheinen die Schlagwörter, die du Lesezeichen und Websites gibst.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = Schlagwörter für { $name }
|
||||||
|
tags-add-label = Schlagwort hinzufügen
|
||||||
|
tags-add-submit = Hinzufügen
|
||||||
|
tags-on-item = Vergeben
|
||||||
|
tags-other = Weitere Schlagwörter
|
||||||
|
tags-remove = Dieses Schlagwort entfernen
|
||||||
|
tags-delete = Dieses Schlagwort überall löschen
|
||||||
|
tags-done = Fertig
|
||||||
|
bookmarks-show-all = Alle Lesezeichen anzeigen
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = Lesezeichen mit dem Schlagwort { $tag }
|
||||||
|
subscriptions-show-all = Alle Websites anzeigen
|
||||||
|
subscriptions-tagged = Websites mit dem Schlagwort { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = Einstellungen
|
||||||
|
settings-font-heading = Schriftart
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = Schriftgröße
|
||||||
|
settings-size-decrease = Kleinere Schrift
|
||||||
|
settings-size-increase = Größere Schrift
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = Darstellung
|
||||||
|
settings-theme-light = Hell
|
||||||
|
settings-theme-dark = Dunkel
|
||||||
|
settings-theme-system = Systemeinstellung folgen
|
||||||
|
settings-variant-heading = Farben
|
||||||
|
settings-variant-site = Website
|
||||||
|
settings-variant-warm = Warm
|
||||||
|
settings-variant-cool = Kühl
|
||||||
|
settings-variant-green = Grün
|
||||||
|
settings-variant-mono = Monochrom
|
||||||
|
settings-language-heading = Sprache
|
||||||
|
settings-language-system = Systemeinstellung folgen
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = Absenden
|
||||||
|
form-reset-default = Zurücksetzen
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = Die Seite konnte nicht geladen werden. Prüfe die Adresse und deine Verbindung und versuche es erneut.
|
||||||
|
error-try-again = Erneut versuchen
|
||||||
|
error-broken-page = Diese Seite hat einen Auszeichnungsfehler und kann deshalb nicht angezeigt werden.
|
||||||
|
error-broken-page-show-text = Seitentext anzeigen
|
||||||
|
error-broken-page-text-title = Seitentext
|
||||||
|
error-non-mews = Dies ist keine Mews-Seite und wird deshalb schlicht angezeigt.
|
||||||
|
error-image-blocked = Dieses Bild liegt auf einer anderen Website und wird nicht angezeigt.
|
||||||
|
error-image-unavailable = Dieses Bild konnte nicht geladen werden.
|
||||||
|
|
@ -138,6 +138,21 @@ settings-variant-warm = Warm
|
||||||
settings-variant-cool = Cool
|
settings-variant-cool = Cool
|
||||||
settings-variant-green = Green
|
settings-variant-green = Green
|
||||||
settings-variant-mono = Mono
|
settings-variant-mono = Mono
|
||||||
|
settings-language-heading = Language
|
||||||
|
settings-language-system = Follow system
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
## Forms
|
## Forms
|
||||||
|
|
||||||
|
|
|
||||||
172
app/locales/es/main.ftl
Normal file
172
app/locales/es/main.ftl
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = Escribe una dirección web
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = Dirección web
|
||||||
|
reader-reload = Recargar esta página
|
||||||
|
reader-open-hub = Abrir tus páginas de Mews
|
||||||
|
reader-back = Atrás
|
||||||
|
reader-forward = Adelante
|
||||||
|
reader-bookmark-add = Añadir marcador
|
||||||
|
reader-bookmark-remove = Quitar marcador
|
||||||
|
reader-subscribe = Suscribirte a este sitio
|
||||||
|
reader-unsubscribe = Cancelar la suscripción
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = Entradas
|
||||||
|
nav-bookmarks = Marcadores
|
||||||
|
nav-subscriptions = Suscripciones
|
||||||
|
nav-history = Historial
|
||||||
|
nav-tags = Etiquetas
|
||||||
|
nav-settings = Ajustes
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = Aún no hay marcadores
|
||||||
|
count-bookmarks-one = 1 marcador
|
||||||
|
count-bookmarks-many = { $count } marcadores
|
||||||
|
count-sites-none = Aún no hay sitios
|
||||||
|
count-sites-one = 1 sitio
|
||||||
|
count-sites-many = { $count } sitios
|
||||||
|
count-pages-none = Aún no hay páginas
|
||||||
|
count-pages-one = 1 página
|
||||||
|
count-pages-many = { $count } páginas
|
||||||
|
count-tags-none = Aún no hay etiquetas
|
||||||
|
count-tags-one = 1 etiqueta
|
||||||
|
count-tags-many = { $count } etiquetas
|
||||||
|
count-unread-none = No hay entradas sin leer
|
||||||
|
count-unread-one = 1 entrada sin leer
|
||||||
|
count-unread-many = { $count } entradas sin leer
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = Entradas
|
||||||
|
posts-empty = Aquí aparecen las entradas de los sitios a los que te suscribes.
|
||||||
|
posts-none-unread = Lo has leído todo.
|
||||||
|
posts-checking = Buscando entradas nuevas.
|
||||||
|
posts-sort-label = Ordenar por
|
||||||
|
posts-sort-date = Fecha
|
||||||
|
posts-sort-source = Sitio
|
||||||
|
posts-sort-unread = Sin leer primero
|
||||||
|
posts-show-label = Mostrar
|
||||||
|
posts-show-all = Todas las entradas
|
||||||
|
posts-show-unread = Solo sin leer
|
||||||
|
posts-mark-read = Marcar como leída
|
||||||
|
posts-mark-unread = Marcar como no leída
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = Buscar entradas nuevas
|
||||||
|
action-search = Buscar
|
||||||
|
action-clear-search = Borrar la búsqueda
|
||||||
|
action-remove-bookmark = Quitar marcador
|
||||||
|
action-unsubscribe = Cancelar la suscripción
|
||||||
|
action-edit-tags = Editar etiquetas
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = Historial
|
||||||
|
history-empty = Aquí aparecen las páginas que visitas.
|
||||||
|
history-no-matches = Ninguna página coincide con tu búsqueda.
|
||||||
|
history-search-label = Buscar en el historial
|
||||||
|
history-forget = Quitar del historial
|
||||||
|
history-clear = Borrar el historial
|
||||||
|
history-clear-confirm = Al borrar el historial se eliminan todas las páginas que has visitado. Esto no se puede deshacer.
|
||||||
|
history-clear-yes = Borrar el historial ahora
|
||||||
|
history-clear-no = Conservar mi historial
|
||||||
|
bookmarks-title = Marcadores
|
||||||
|
bookmarks-empty = Aquí aparecen las páginas que guardas como marcador.
|
||||||
|
bookmarks-no-matches = Ningún marcador coincide con tu búsqueda.
|
||||||
|
bookmarks-search-label = Buscar en los marcadores
|
||||||
|
subscriptions-title = Suscripciones
|
||||||
|
subscriptions-empty = Aquí aparecen los sitios a los que te suscribes.
|
||||||
|
subscriptions-no-matches = Ningún sitio coincide con tu búsqueda.
|
||||||
|
subscriptions-search-label = Buscar sitios
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = Última comprobación { $date }
|
||||||
|
subscriptions-never-checked = Sin comprobar todavía
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = Etiquetas
|
||||||
|
tags-empty = Aquí aparecen las etiquetas que añades a marcadores y sitios.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = Etiquetas de { $name }
|
||||||
|
tags-add-label = Añadir una etiqueta
|
||||||
|
tags-add-submit = Añadir
|
||||||
|
tags-on-item = En este elemento
|
||||||
|
tags-other = Otras etiquetas
|
||||||
|
tags-remove = Quitar esta etiqueta
|
||||||
|
tags-delete = Eliminar esta etiqueta en todas partes
|
||||||
|
tags-done = Listo
|
||||||
|
bookmarks-show-all = Mostrar todos los marcadores
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = Marcadores con la etiqueta { $tag }
|
||||||
|
subscriptions-show-all = Mostrar todos los sitios
|
||||||
|
subscriptions-tagged = Sitios con la etiqueta { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = Ajustes
|
||||||
|
settings-font-heading = Tipografía
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = Tamaño del texto
|
||||||
|
settings-size-decrease = Texto más pequeño
|
||||||
|
settings-size-increase = Texto más grande
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = Tema
|
||||||
|
settings-theme-light = Claro
|
||||||
|
settings-theme-dark = Oscuro
|
||||||
|
settings-theme-system = Seguir al sistema
|
||||||
|
settings-variant-heading = Colores
|
||||||
|
settings-variant-site = Sitio
|
||||||
|
settings-variant-warm = Cálido
|
||||||
|
settings-variant-cool = Frío
|
||||||
|
settings-variant-green = Verde
|
||||||
|
settings-variant-mono = Monocromo
|
||||||
|
settings-language-heading = Idioma
|
||||||
|
settings-language-system = Seguir al sistema
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = Enviar
|
||||||
|
form-reset-default = Restablecer
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = No se ha podido cargar la página. Comprueba la dirección y tu conexión, y vuelve a intentarlo.
|
||||||
|
error-try-again = Volver a intentarlo
|
||||||
|
error-broken-page = Esta página tiene un error de marcado, así que no se puede mostrar.
|
||||||
|
error-broken-page-show-text = Mostrar el texto de la página
|
||||||
|
error-broken-page-text-title = Texto de la página
|
||||||
|
error-non-mews = Esta página no es una página de Mews, así que se muestra de forma sencilla.
|
||||||
|
error-image-blocked = Esta imagen está en otro sitio y no se muestra.
|
||||||
|
error-image-unavailable = No se ha podido cargar esta imagen.
|
||||||
172
app/locales/fr/main.ftl
Normal file
172
app/locales/fr/main.ftl
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = Saisis une adresse web
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = Adresse web
|
||||||
|
reader-reload = Recharger cette page
|
||||||
|
reader-open-hub = Ouvrir tes pages Mews
|
||||||
|
reader-back = Revenir en arrière
|
||||||
|
reader-forward = Aller en avant
|
||||||
|
reader-bookmark-add = Ajouter un signet
|
||||||
|
reader-bookmark-remove = Retirer le signet
|
||||||
|
reader-subscribe = S'abonner à ce site
|
||||||
|
reader-unsubscribe = Se désabonner
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = Billets
|
||||||
|
nav-bookmarks = Signets
|
||||||
|
nav-subscriptions = Abonnements
|
||||||
|
nav-history = Historique
|
||||||
|
nav-tags = Étiquettes
|
||||||
|
nav-settings = Réglages
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = Aucun signet pour l'instant
|
||||||
|
count-bookmarks-one = { $count } signet
|
||||||
|
count-bookmarks-many = { $count } signets
|
||||||
|
count-sites-none = Aucun site pour l'instant
|
||||||
|
count-sites-one = { $count } site
|
||||||
|
count-sites-many = { $count } sites
|
||||||
|
count-pages-none = Aucune page pour l'instant
|
||||||
|
count-pages-one = { $count } page
|
||||||
|
count-pages-many = { $count } pages
|
||||||
|
count-tags-none = Aucune étiquette pour l'instant
|
||||||
|
count-tags-one = { $count } étiquette
|
||||||
|
count-tags-many = { $count } étiquettes
|
||||||
|
count-unread-none = Aucun billet non lu
|
||||||
|
count-unread-one = { $count } billet non lu
|
||||||
|
count-unread-many = { $count } billets non lus
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = Billets
|
||||||
|
posts-empty = Les billets des sites auxquels tu es abonné apparaissent ici.
|
||||||
|
posts-none-unread = Tu as tout lu.
|
||||||
|
posts-checking = Recherche de nouveaux billets en cours.
|
||||||
|
posts-sort-label = Trier par
|
||||||
|
posts-sort-date = Date
|
||||||
|
posts-sort-source = Site
|
||||||
|
posts-sort-unread = Non lus d'abord
|
||||||
|
posts-show-label = Afficher
|
||||||
|
posts-show-all = Tous les billets
|
||||||
|
posts-show-unread = Non lus seulement
|
||||||
|
posts-mark-read = Marquer comme lu
|
||||||
|
posts-mark-unread = Marquer comme non lu
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = Chercher de nouveaux billets
|
||||||
|
action-search = Rechercher
|
||||||
|
action-clear-search = Effacer la recherche
|
||||||
|
action-remove-bookmark = Retirer le signet
|
||||||
|
action-unsubscribe = Se désabonner
|
||||||
|
action-edit-tags = Modifier les étiquettes
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = Historique
|
||||||
|
history-empty = Les pages que tu visites apparaissent ici.
|
||||||
|
history-no-matches = Aucune page ne correspond à ta recherche.
|
||||||
|
history-search-label = Rechercher dans l'historique
|
||||||
|
history-forget = Retirer de l'historique
|
||||||
|
history-clear = Effacer l'historique
|
||||||
|
history-clear-confirm = Effacer l'historique supprime toutes les pages visitées. Cette action est définitive.
|
||||||
|
history-clear-yes = Effacer l'historique maintenant
|
||||||
|
history-clear-no = Garder mon historique
|
||||||
|
bookmarks-title = Signets
|
||||||
|
bookmarks-empty = Les pages que tu mets en signet apparaissent ici.
|
||||||
|
bookmarks-no-matches = Aucun signet ne correspond à ta recherche.
|
||||||
|
bookmarks-search-label = Rechercher dans les signets
|
||||||
|
subscriptions-title = Abonnements
|
||||||
|
subscriptions-empty = Les sites auxquels tu t'abonnes apparaissent ici.
|
||||||
|
subscriptions-no-matches = Aucun site ne correspond à ta recherche.
|
||||||
|
subscriptions-search-label = Rechercher un site
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = Dernière vérification { $date }
|
||||||
|
subscriptions-never-checked = Pas encore vérifié
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = Étiquettes
|
||||||
|
tags-empty = Les étiquettes que tu ajoutes aux signets et aux sites apparaissent ici.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = Étiquettes de { $name }
|
||||||
|
tags-add-label = Ajouter une étiquette
|
||||||
|
tags-add-submit = Ajouter
|
||||||
|
tags-on-item = Sur cet élément
|
||||||
|
tags-other = Autres étiquettes
|
||||||
|
tags-remove = Retirer cette étiquette
|
||||||
|
tags-delete = Supprimer cette étiquette partout
|
||||||
|
tags-done = Terminé
|
||||||
|
bookmarks-show-all = Afficher tous les signets
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = Signets portant l'étiquette { $tag }
|
||||||
|
subscriptions-show-all = Afficher tous les sites
|
||||||
|
subscriptions-tagged = Sites portant l'étiquette { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = Réglages
|
||||||
|
settings-font-heading = Police
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = Taille du texte
|
||||||
|
settings-size-decrease = Texte plus petit
|
||||||
|
settings-size-increase = Texte plus grand
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = Thème
|
||||||
|
settings-theme-light = Clair
|
||||||
|
settings-theme-dark = Sombre
|
||||||
|
settings-theme-system = Suivre le système
|
||||||
|
settings-variant-heading = Couleurs
|
||||||
|
settings-variant-site = Site
|
||||||
|
settings-variant-warm = Chaud
|
||||||
|
settings-variant-cool = Froid
|
||||||
|
settings-variant-green = Vert
|
||||||
|
settings-variant-mono = Monochrome
|
||||||
|
settings-language-heading = Langue
|
||||||
|
settings-language-system = Suivre le système
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = Envoyer
|
||||||
|
form-reset-default = Réinitialiser
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = La page n'a pas pu être chargée. Vérifie l'adresse et ta connexion, puis réessaie.
|
||||||
|
error-try-again = Réessayer
|
||||||
|
error-broken-page = Cette page comporte une erreur de balisage et ne peut donc pas être affichée.
|
||||||
|
error-broken-page-show-text = Afficher le texte de la page
|
||||||
|
error-broken-page-text-title = Texte de la page
|
||||||
|
error-non-mews = Cette page n'est pas une page Mews, elle est donc affichée simplement.
|
||||||
|
error-image-blocked = Cette image se trouve sur un autre site et n'est pas affichée.
|
||||||
|
error-image-unavailable = Cette image n'a pas pu être chargée.
|
||||||
177
app/locales/he/main.ftl
Normal file
177
app/locales/he/main.ftl
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = הזינו כתובת אינטרנט
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = כתובת אינטרנט
|
||||||
|
reader-reload = טעינה מחדש של הדף
|
||||||
|
reader-open-hub = פתיחת דפי Mews שלכם
|
||||||
|
reader-back = חזרה
|
||||||
|
reader-forward = קדימה
|
||||||
|
reader-bookmark-add = הוספת סימנייה
|
||||||
|
reader-bookmark-remove = הסרת הסימנייה
|
||||||
|
reader-subscribe = הרשמה לאתר הזה
|
||||||
|
reader-unsubscribe = ביטול ההרשמה
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = רשומות
|
||||||
|
nav-bookmarks = סימניות
|
||||||
|
nav-subscriptions = הרשמות
|
||||||
|
nav-history = היסטוריה
|
||||||
|
nav-tags = תגיות
|
||||||
|
nav-settings = הגדרות
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = אין עדיין סימניות
|
||||||
|
count-bookmarks-one = סימנייה אחת
|
||||||
|
count-bookmarks-many = { $count } סימניות
|
||||||
|
count-bookmarks-two = שתי סימניות
|
||||||
|
count-sites-none = אין עדיין אתרים
|
||||||
|
count-sites-one = אתר אחד
|
||||||
|
count-sites-many = { $count } אתרים
|
||||||
|
count-sites-two = שני אתרים
|
||||||
|
count-pages-none = אין עדיין דפים
|
||||||
|
count-pages-one = דף אחד
|
||||||
|
count-pages-many = { $count } דפים
|
||||||
|
count-pages-two = שני דפים
|
||||||
|
count-tags-none = אין עדיין תגיות
|
||||||
|
count-tags-one = תגית אחת
|
||||||
|
count-tags-many = { $count } תגיות
|
||||||
|
count-tags-two = שתי תגיות
|
||||||
|
count-unread-none = אין רשומות שלא נקראו
|
||||||
|
count-unread-one = רשומה אחת שלא נקראה
|
||||||
|
count-unread-many = { $count } רשומות שלא נקראו
|
||||||
|
count-unread-two = שתי רשומות שלא נקראו
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = רשומות
|
||||||
|
posts-empty = רשומות מהאתרים שנרשמתם אליהם יופיעו כאן.
|
||||||
|
posts-none-unread = קראתם הכול.
|
||||||
|
posts-checking = מתבצעת בדיקה אחר רשומות חדשות.
|
||||||
|
posts-sort-label = מיון לפי
|
||||||
|
posts-sort-date = תאריך
|
||||||
|
posts-sort-source = אתר
|
||||||
|
posts-sort-unread = שלא נקראו תחילה
|
||||||
|
posts-show-label = הצגה
|
||||||
|
posts-show-all = כל הרשומות
|
||||||
|
posts-show-unread = שלא נקראו בלבד
|
||||||
|
posts-mark-read = סימון כנקראה
|
||||||
|
posts-mark-unread = סימון כלא נקראה
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = בדיקה אחר רשומות חדשות
|
||||||
|
action-search = חיפוש
|
||||||
|
action-clear-search = ניקוי החיפוש
|
||||||
|
action-remove-bookmark = הסרת הסימנייה
|
||||||
|
action-unsubscribe = ביטול ההרשמה
|
||||||
|
action-edit-tags = עריכת התגיות
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = היסטוריה
|
||||||
|
history-empty = הדפים שתבקרו בהם יופיעו כאן.
|
||||||
|
history-no-matches = אין דפים התואמים לחיפוש.
|
||||||
|
history-search-label = חיפוש בהיסטוריה
|
||||||
|
history-forget = הסרה מההיסטוריה
|
||||||
|
history-clear = ניקוי ההיסטוריה
|
||||||
|
history-clear-confirm = ניקוי ההיסטוריה מסיר כל דף שביקרתם בו. אי אפשר לבטל את הפעולה.
|
||||||
|
history-clear-yes = לנקות את ההיסטוריה
|
||||||
|
history-clear-no = להשאיר את ההיסטוריה
|
||||||
|
bookmarks-title = סימניות
|
||||||
|
bookmarks-empty = הדפים שתסמנו יופיעו כאן.
|
||||||
|
bookmarks-no-matches = אין סימניות התואמות לחיפוש.
|
||||||
|
bookmarks-search-label = חיפוש בסימניות
|
||||||
|
subscriptions-title = הרשמות
|
||||||
|
subscriptions-empty = האתרים שתירשמו אליהם יופיעו כאן.
|
||||||
|
subscriptions-no-matches = אין אתרים התואמים לחיפוש.
|
||||||
|
subscriptions-search-label = חיפוש אתרים
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = נבדק לאחרונה { $date }
|
||||||
|
subscriptions-never-checked = טרם נבדק
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = תגיות
|
||||||
|
tags-empty = התגיות שתוסיפו לסימניות ולאתרים יופיעו כאן.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = תגיות עבור { $name }
|
||||||
|
tags-add-label = הוספת תגית
|
||||||
|
tags-add-submit = הוספה
|
||||||
|
tags-on-item = על הפריט הזה
|
||||||
|
tags-other = תגיות נוספות
|
||||||
|
tags-remove = הסרת התגית הזו
|
||||||
|
tags-delete = מחיקת התגית הזו מכל מקום
|
||||||
|
tags-done = סיום
|
||||||
|
bookmarks-show-all = הצגת כל הסימניות
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = סימניות עם התגית { $tag }
|
||||||
|
subscriptions-show-all = הצגת כל האתרים
|
||||||
|
subscriptions-tagged = אתרים עם התגית { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = הגדרות
|
||||||
|
settings-font-heading = גופן
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = גודל הטקסט
|
||||||
|
settings-size-decrease = טקסט קטן יותר
|
||||||
|
settings-size-increase = טקסט גדול יותר
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = ערכת נושא
|
||||||
|
settings-theme-light = בהירה
|
||||||
|
settings-theme-dark = כהה
|
||||||
|
settings-theme-system = לפי המערכת
|
||||||
|
settings-variant-heading = צבעים
|
||||||
|
settings-variant-site = של האתר
|
||||||
|
settings-variant-warm = חמים
|
||||||
|
settings-variant-cool = קרים
|
||||||
|
settings-variant-green = ירוקים
|
||||||
|
settings-variant-mono = מונוכרום
|
||||||
|
settings-language-heading = שפה
|
||||||
|
settings-language-system = לפי המערכת
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = שליחה
|
||||||
|
form-reset-default = איפוס
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = לא ניתן לטעון את הדף. בדקו את הכתובת ואת החיבור ונסו שוב.
|
||||||
|
error-try-again = ניסיון חוזר
|
||||||
|
error-broken-page = בדף הזה יש שגיאת סימון, ולכן אי אפשר להציג אותו.
|
||||||
|
error-broken-page-show-text = הצגת הטקסט של הדף
|
||||||
|
error-broken-page-text-title = הטקסט של הדף
|
||||||
|
error-non-mews = הדף הזה אינו דף Mews, ולכן הוא מוצג בפשטות.
|
||||||
|
error-image-blocked = התמונה הזו נמצאת באתר אחר ואינה מוצגת.
|
||||||
|
error-image-unavailable = לא ניתן לטעון את התמונה הזו.
|
||||||
172
app/locales/ja/main.ftl
Normal file
172
app/locales/ja/main.ftl
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = ウェブアドレスを入力
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = ウェブアドレス
|
||||||
|
reader-reload = このページを再読み込み
|
||||||
|
reader-open-hub = Mews のページを開く
|
||||||
|
reader-back = 戻る
|
||||||
|
reader-forward = 進む
|
||||||
|
reader-bookmark-add = ブックマークに追加
|
||||||
|
reader-bookmark-remove = ブックマークを削除
|
||||||
|
reader-subscribe = このサイトを購読
|
||||||
|
reader-unsubscribe = 購読をやめる
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = 投稿
|
||||||
|
nav-bookmarks = ブックマーク
|
||||||
|
nav-subscriptions = 購読
|
||||||
|
nav-history = 履歴
|
||||||
|
nav-tags = タグ
|
||||||
|
nav-settings = 設定
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = ブックマークはまだありません
|
||||||
|
count-bookmarks-one = ブックマーク { $count } 件
|
||||||
|
count-bookmarks-many = ブックマーク { $count } 件
|
||||||
|
count-sites-none = サイトはまだありません
|
||||||
|
count-sites-one = サイト { $count } 件
|
||||||
|
count-sites-many = サイト { $count } 件
|
||||||
|
count-pages-none = ページはまだありません
|
||||||
|
count-pages-one = ページ { $count } 件
|
||||||
|
count-pages-many = ページ { $count } 件
|
||||||
|
count-tags-none = タグはまだありません
|
||||||
|
count-tags-one = タグ { $count } 件
|
||||||
|
count-tags-many = タグ { $count } 件
|
||||||
|
count-unread-none = 未読の投稿はありません
|
||||||
|
count-unread-one = 未読の投稿 { $count } 件
|
||||||
|
count-unread-many = 未読の投稿 { $count } 件
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = 投稿
|
||||||
|
posts-empty = 購読しているサイトの投稿がここに表示されます。
|
||||||
|
posts-none-unread = すべて読み終えました。
|
||||||
|
posts-checking = 新しい投稿を確認しています。
|
||||||
|
posts-sort-label = 並べ替え
|
||||||
|
posts-sort-date = 日付
|
||||||
|
posts-sort-source = サイト
|
||||||
|
posts-sort-unread = 未読を先に
|
||||||
|
posts-show-label = 表示
|
||||||
|
posts-show-all = すべての投稿
|
||||||
|
posts-show-unread = 未読のみ
|
||||||
|
posts-mark-read = 既読にする
|
||||||
|
posts-mark-unread = 未読にする
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = 新しい投稿を確認
|
||||||
|
action-search = 検索
|
||||||
|
action-clear-search = 検索を消去
|
||||||
|
action-remove-bookmark = ブックマークを削除
|
||||||
|
action-unsubscribe = 購読をやめる
|
||||||
|
action-edit-tags = タグを編集
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = 履歴
|
||||||
|
history-empty = 訪れたページがここに表示されます。
|
||||||
|
history-no-matches = 検索に一致するページはありません。
|
||||||
|
history-search-label = 履歴を検索
|
||||||
|
history-forget = 履歴から削除
|
||||||
|
history-clear = 履歴を消去
|
||||||
|
history-clear-confirm = 履歴を消去すると、訪れたページがすべて削除されます。元に戻せません。
|
||||||
|
history-clear-yes = 履歴を消去する
|
||||||
|
history-clear-no = 履歴を残す
|
||||||
|
bookmarks-title = ブックマーク
|
||||||
|
bookmarks-empty = ブックマークしたページがここに表示されます。
|
||||||
|
bookmarks-no-matches = 検索に一致するブックマークはありません。
|
||||||
|
bookmarks-search-label = ブックマークを検索
|
||||||
|
subscriptions-title = 購読
|
||||||
|
subscriptions-empty = 購読しているサイトがここに表示されます。
|
||||||
|
subscriptions-no-matches = 検索に一致するサイトはありません。
|
||||||
|
subscriptions-search-label = サイトを検索
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = 最終確認 { $date }
|
||||||
|
subscriptions-never-checked = 未確認
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = タグ
|
||||||
|
tags-empty = ブックマークやサイトに付けたタグがここに表示されます。
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = { $name } のタグ
|
||||||
|
tags-add-label = タグを追加
|
||||||
|
tags-add-submit = 追加
|
||||||
|
tags-on-item = この項目のタグ
|
||||||
|
tags-other = ほかのタグ
|
||||||
|
tags-remove = このタグを外す
|
||||||
|
tags-delete = このタグをすべてから削除
|
||||||
|
tags-done = 完了
|
||||||
|
bookmarks-show-all = すべてのブックマークを表示
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = タグ { $tag } のブックマーク
|
||||||
|
subscriptions-show-all = すべてのサイトを表示
|
||||||
|
subscriptions-tagged = タグ { $tag } のサイト
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = 設定
|
||||||
|
settings-font-heading = 書体
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = 文字の大きさ
|
||||||
|
settings-size-decrease = 小さく
|
||||||
|
settings-size-increase = 大きく
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = テーマ
|
||||||
|
settings-theme-light = ライト
|
||||||
|
settings-theme-dark = ダーク
|
||||||
|
settings-theme-system = システムに合わせる
|
||||||
|
settings-variant-heading = 配色
|
||||||
|
settings-variant-site = サイト
|
||||||
|
settings-variant-warm = 暖色
|
||||||
|
settings-variant-cool = 寒色
|
||||||
|
settings-variant-green = グリーン
|
||||||
|
settings-variant-mono = モノクロ
|
||||||
|
settings-language-heading = 言語
|
||||||
|
settings-language-system = システムに合わせる
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = 送信
|
||||||
|
form-reset-default = リセット
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = ページを読み込めませんでした。アドレスと接続を確認して、もう一度お試しください。
|
||||||
|
error-try-again = もう一度試す
|
||||||
|
error-broken-page = このページにはマークアップの誤りがあるため、表示できません。
|
||||||
|
error-broken-page-show-text = ページのテキストを表示
|
||||||
|
error-broken-page-text-title = ページのテキスト
|
||||||
|
error-non-mews = これは Mews のページではないため、簡素に表示しています。
|
||||||
|
error-image-blocked = この画像は別のサイトにあるため表示しません。
|
||||||
|
error-image-unavailable = この画像を読み込めませんでした。
|
||||||
172
app/locales/pt/main.ftl
Normal file
172
app/locales/pt/main.ftl
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = Escreve um endereço web
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = Endereço web
|
||||||
|
reader-reload = Recarregar esta página
|
||||||
|
reader-open-hub = Abrir as tuas páginas Mews
|
||||||
|
reader-back = Retroceder
|
||||||
|
reader-forward = Avançar
|
||||||
|
reader-bookmark-add = Adicionar marcador
|
||||||
|
reader-bookmark-remove = Remover marcador
|
||||||
|
reader-subscribe = Subscrever este site
|
||||||
|
reader-unsubscribe = Cancelar a subscrição
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = Publicações
|
||||||
|
nav-bookmarks = Marcadores
|
||||||
|
nav-subscriptions = Subscrições
|
||||||
|
nav-history = Histórico
|
||||||
|
nav-tags = Etiquetas
|
||||||
|
nav-settings = Definições
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = Ainda não há marcadores
|
||||||
|
count-bookmarks-one = { $count } marcador
|
||||||
|
count-bookmarks-many = { $count } marcadores
|
||||||
|
count-sites-none = Ainda não há sites
|
||||||
|
count-sites-one = { $count } site
|
||||||
|
count-sites-many = { $count } sites
|
||||||
|
count-pages-none = Ainda não há páginas
|
||||||
|
count-pages-one = { $count } página
|
||||||
|
count-pages-many = { $count } páginas
|
||||||
|
count-tags-none = Ainda não há etiquetas
|
||||||
|
count-tags-one = { $count } etiqueta
|
||||||
|
count-tags-many = { $count } etiquetas
|
||||||
|
count-unread-none = Não há publicações por ler
|
||||||
|
count-unread-one = { $count } publicação por ler
|
||||||
|
count-unread-many = { $count } publicações por ler
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = Publicações
|
||||||
|
posts-empty = Aqui aparecem as publicações dos sites que subscreves.
|
||||||
|
posts-none-unread = Já leste tudo.
|
||||||
|
posts-checking = A procurar publicações novas.
|
||||||
|
posts-sort-label = Ordenar por
|
||||||
|
posts-sort-date = Data
|
||||||
|
posts-sort-source = Site
|
||||||
|
posts-sort-unread = Por ler primeiro
|
||||||
|
posts-show-label = Mostrar
|
||||||
|
posts-show-all = Todas as publicações
|
||||||
|
posts-show-unread = Apenas por ler
|
||||||
|
posts-mark-read = Marcar como lida
|
||||||
|
posts-mark-unread = Marcar como não lida
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = Procurar publicações novas
|
||||||
|
action-search = Procurar
|
||||||
|
action-clear-search = Limpar a procura
|
||||||
|
action-remove-bookmark = Remover marcador
|
||||||
|
action-unsubscribe = Cancelar a subscrição
|
||||||
|
action-edit-tags = Editar etiquetas
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = Histórico
|
||||||
|
history-empty = Aqui aparecem as páginas que visitas.
|
||||||
|
history-no-matches = Nenhuma página corresponde à tua procura.
|
||||||
|
history-search-label = Procurar no histórico
|
||||||
|
history-forget = Remover do histórico
|
||||||
|
history-clear = Limpar o histórico
|
||||||
|
history-clear-confirm = Limpar o histórico remove todas as páginas que visitaste. Não é possível anular.
|
||||||
|
history-clear-yes = Limpar o histórico agora
|
||||||
|
history-clear-no = Manter o meu histórico
|
||||||
|
bookmarks-title = Marcadores
|
||||||
|
bookmarks-empty = Aqui aparecem as páginas que guardas como marcador.
|
||||||
|
bookmarks-no-matches = Nenhum marcador corresponde à tua procura.
|
||||||
|
bookmarks-search-label = Procurar nos marcadores
|
||||||
|
subscriptions-title = Subscrições
|
||||||
|
subscriptions-empty = Aqui aparecem os sites que subscreves.
|
||||||
|
subscriptions-no-matches = Nenhum site corresponde à tua procura.
|
||||||
|
subscriptions-search-label = Procurar sites
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = Última verificação { $date }
|
||||||
|
subscriptions-never-checked = Ainda não verificado
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = Etiquetas
|
||||||
|
tags-empty = Aqui aparecem as etiquetas que dás a marcadores e sites.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = Etiquetas de { $name }
|
||||||
|
tags-add-label = Adicionar uma etiqueta
|
||||||
|
tags-add-submit = Adicionar
|
||||||
|
tags-on-item = Neste item
|
||||||
|
tags-other = Outras etiquetas
|
||||||
|
tags-remove = Remover esta etiqueta
|
||||||
|
tags-delete = Eliminar esta etiqueta em todo o lado
|
||||||
|
tags-done = Concluído
|
||||||
|
bookmarks-show-all = Mostrar todos os marcadores
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = Marcadores com a etiqueta { $tag }
|
||||||
|
subscriptions-show-all = Mostrar todos os sites
|
||||||
|
subscriptions-tagged = Sites com a etiqueta { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = Definições
|
||||||
|
settings-font-heading = Tipo de letra
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = Tamanho do texto
|
||||||
|
settings-size-decrease = Texto mais pequeno
|
||||||
|
settings-size-increase = Texto maior
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = Tema
|
||||||
|
settings-theme-light = Claro
|
||||||
|
settings-theme-dark = Escuro
|
||||||
|
settings-theme-system = Seguir o sistema
|
||||||
|
settings-variant-heading = Cores
|
||||||
|
settings-variant-site = Site
|
||||||
|
settings-variant-warm = Quente
|
||||||
|
settings-variant-cool = Frio
|
||||||
|
settings-variant-green = Verde
|
||||||
|
settings-variant-mono = Monocromático
|
||||||
|
settings-language-heading = Idioma
|
||||||
|
settings-language-system = Seguir o sistema
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = Enviar
|
||||||
|
form-reset-default = Repor
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = Não foi possível carregar a página. Verifica o endereço e a tua ligação e tenta de novo.
|
||||||
|
error-try-again = Tentar de novo
|
||||||
|
error-broken-page = Esta página tem um erro de marcação, por isso não pode ser mostrada.
|
||||||
|
error-broken-page-show-text = Mostrar o texto da página
|
||||||
|
error-broken-page-text-title = Texto da página
|
||||||
|
error-non-mews = Esta não é uma página Mews, por isso é mostrada de forma simples.
|
||||||
|
error-image-blocked = Esta imagem está noutro site e não é mostrada.
|
||||||
|
error-image-unavailable = Não foi possível carregar esta imagem.
|
||||||
177
app/locales/ru/main.ftl
Normal file
177
app/locales/ru/main.ftl
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
## Reader chrome
|
||||||
|
|
||||||
|
reader-address-placeholder = Введите веб-адрес
|
||||||
|
# Accessibility label for the address field.
|
||||||
|
reader-address-label = Веб-адрес
|
||||||
|
reader-reload = Обновить эту страницу
|
||||||
|
reader-open-hub = Открыть ваши страницы Mews
|
||||||
|
reader-back = Назад
|
||||||
|
reader-forward = Вперёд
|
||||||
|
reader-bookmark-add = Добавить закладку
|
||||||
|
reader-bookmark-remove = Удалить закладку
|
||||||
|
reader-subscribe = Подписаться на этот сайт
|
||||||
|
reader-unsubscribe = Отписаться
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
# The strip of the app's own pages, carried by each of them.
|
||||||
|
nav-hub = Mews
|
||||||
|
nav-posts = Записи
|
||||||
|
nav-bookmarks = Закладки
|
||||||
|
nav-subscriptions = Подписки
|
||||||
|
nav-history = История
|
||||||
|
nav-tags = Метки
|
||||||
|
nav-settings = Настройки
|
||||||
|
|
||||||
|
## Hub
|
||||||
|
|
||||||
|
hub-title = Mews
|
||||||
|
|
||||||
|
## Counts
|
||||||
|
|
||||||
|
# Shown under an item. Format the number in code.
|
||||||
|
count-bookmarks-none = Закладок пока нет
|
||||||
|
count-bookmarks-one = { $count } закладка
|
||||||
|
count-bookmarks-many = { $count } закладок
|
||||||
|
count-bookmarks-few = { $count } закладки
|
||||||
|
count-sites-none = Сайтов пока нет
|
||||||
|
count-sites-one = { $count } сайт
|
||||||
|
count-sites-many = { $count } сайтов
|
||||||
|
count-sites-few = { $count } сайта
|
||||||
|
count-pages-none = Страниц пока нет
|
||||||
|
count-pages-one = { $count } страница
|
||||||
|
count-pages-many = { $count } страниц
|
||||||
|
count-pages-few = { $count } страницы
|
||||||
|
count-tags-none = Меток пока нет
|
||||||
|
count-tags-one = { $count } метка
|
||||||
|
count-tags-many = { $count } меток
|
||||||
|
count-tags-few = { $count } метки
|
||||||
|
count-unread-none = Непрочитанных записей нет
|
||||||
|
count-unread-one = { $count } непрочитанная запись
|
||||||
|
count-unread-many = { $count } непрочитанных записей
|
||||||
|
count-unread-few = { $count } непрочитанные записи
|
||||||
|
|
||||||
|
## Posts
|
||||||
|
|
||||||
|
posts-title = Записи
|
||||||
|
posts-empty = Здесь появятся записи сайтов, на которые вы подписаны.
|
||||||
|
posts-none-unread = Вы всё прочитали.
|
||||||
|
posts-checking = Идёт проверка новых записей.
|
||||||
|
posts-sort-label = Сортировка
|
||||||
|
posts-sort-date = По дате
|
||||||
|
posts-sort-source = По сайту
|
||||||
|
posts-sort-unread = Непрочитанные сверху
|
||||||
|
posts-show-label = Показывать
|
||||||
|
posts-show-all = Все записи
|
||||||
|
posts-show-unread = Только непрочитанные
|
||||||
|
posts-mark-read = Отметить прочитанной
|
||||||
|
posts-mark-unread = Отметить непрочитанной
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
# Link labels used on more than one page.
|
||||||
|
action-refresh = Проверить новые записи
|
||||||
|
action-search = Искать
|
||||||
|
action-clear-search = Очистить поиск
|
||||||
|
action-remove-bookmark = Удалить закладку
|
||||||
|
action-unsubscribe = Отписаться
|
||||||
|
action-edit-tags = Изменить метки
|
||||||
|
|
||||||
|
## Library pages
|
||||||
|
|
||||||
|
# Internal pages rendered like ordinary Mews pages.
|
||||||
|
history-title = История
|
||||||
|
history-empty = Здесь появятся страницы, которые вы открываете.
|
||||||
|
history-no-matches = Ни одна страница не найдена по запросу.
|
||||||
|
history-search-label = Поиск по истории
|
||||||
|
history-forget = Удалить из истории
|
||||||
|
history-clear = Очистить историю
|
||||||
|
history-clear-confirm = При очистке истории будут удалены все посещённые страницы. Это действие необратимо.
|
||||||
|
history-clear-yes = Очистить историю
|
||||||
|
history-clear-no = Оставить историю
|
||||||
|
bookmarks-title = Закладки
|
||||||
|
bookmarks-empty = Здесь появятся страницы, которые вы добавите в закладки.
|
||||||
|
bookmarks-no-matches = Ни одна закладка не найдена по запросу.
|
||||||
|
bookmarks-search-label = Поиск по закладкам
|
||||||
|
subscriptions-title = Подписки
|
||||||
|
subscriptions-empty = Здесь появятся сайты, на которые вы подпишетесь.
|
||||||
|
subscriptions-no-matches = Ни один сайт не найден по запросу.
|
||||||
|
subscriptions-search-label = Поиск по сайтам
|
||||||
|
# When a site was last checked for posts. Format the date in code.
|
||||||
|
subscriptions-checked = Последняя проверка { $date }
|
||||||
|
subscriptions-never-checked = Ещё не проверялся
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
|
||||||
|
tags-title = Метки
|
||||||
|
tags-empty = Здесь появятся метки, которые вы добавите к закладкам и сайтам.
|
||||||
|
# The item being tagged. Shows its title, or its address until it has one.
|
||||||
|
tags-assign-title = Метки для { $name }
|
||||||
|
tags-add-label = Добавить метку
|
||||||
|
tags-add-submit = Добавить
|
||||||
|
tags-on-item = У этого элемента
|
||||||
|
tags-other = Другие метки
|
||||||
|
tags-remove = Убрать эту метку
|
||||||
|
tags-delete = Удалить эту метку везде
|
||||||
|
tags-done = Готово
|
||||||
|
bookmarks-show-all = Показать все закладки
|
||||||
|
# Shown when a tag narrows the list.
|
||||||
|
bookmarks-tagged = Закладки с меткой { $tag }
|
||||||
|
subscriptions-show-all = Показать все сайты
|
||||||
|
subscriptions-tagged = Сайты с меткой { $tag }
|
||||||
|
|
||||||
|
## Settings page
|
||||||
|
|
||||||
|
settings-page-title = Настройки
|
||||||
|
settings-font-heading = Шрифт
|
||||||
|
settings-font-atkinson = Atkinson Hyperlegible
|
||||||
|
settings-font-luciole = Luciole
|
||||||
|
settings-font-literata = Literata
|
||||||
|
settings-font-charis = Charis SIL
|
||||||
|
settings-size-heading = Размер текста
|
||||||
|
settings-size-decrease = Мельче
|
||||||
|
settings-size-increase = Крупнее
|
||||||
|
# The text size now in use. Format the number in code.
|
||||||
|
settings-size-value = { $percent }%
|
||||||
|
settings-theme-heading = Оформление
|
||||||
|
settings-theme-light = Светлое
|
||||||
|
settings-theme-dark = Тёмное
|
||||||
|
settings-theme-system = Как в системе
|
||||||
|
settings-variant-heading = Цвета
|
||||||
|
settings-variant-site = Сайта
|
||||||
|
settings-variant-warm = Тёплые
|
||||||
|
settings-variant-cool = Холодные
|
||||||
|
settings-variant-green = Зелёные
|
||||||
|
settings-variant-mono = Монохромные
|
||||||
|
settings-language-heading = Язык
|
||||||
|
settings-language-system = Как в системе
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
# Every language is named in its own words, so a reader who cannot read the
|
||||||
|
# current interface can still find theirs. Leave these untranslated.
|
||||||
|
language-en = English
|
||||||
|
language-de = Deutsch
|
||||||
|
language-es = Español
|
||||||
|
language-fr = Français
|
||||||
|
language-he = עברית
|
||||||
|
language-ja = 日本語
|
||||||
|
language-pt = Português
|
||||||
|
language-ru = Русский
|
||||||
|
|
||||||
|
## Forms
|
||||||
|
|
||||||
|
# Default label for a page form's submit control when the author gave none.
|
||||||
|
form-submit-default = Отправить
|
||||||
|
form-reset-default = Сбросить
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
error-load-failed = Не удалось загрузить страницу. Проверьте адрес и подключение, затем повторите попытку.
|
||||||
|
error-try-again = Повторить попытку
|
||||||
|
error-broken-page = На этой странице ошибка разметки, поэтому показать её нельзя.
|
||||||
|
error-broken-page-show-text = Показать текст страницы
|
||||||
|
error-broken-page-text-title = Текст страницы
|
||||||
|
error-non-mews = Это не страница Mews, поэтому она показана простым текстом.
|
||||||
|
error-image-blocked = Это изображение находится на другом сайте и не показывается.
|
||||||
|
error-image-unavailable = Не удалось загрузить изображение.
|
||||||
|
|
@ -254,6 +254,11 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -311,10 +316,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.3"
|
version: "0.20.2"
|
||||||
io:
|
io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
fluent: ^2.0.4
|
fluent: ^2.0.4
|
||||||
signals: ^7.1.0
|
signals: ^7.1.0
|
||||||
|
|
@ -34,6 +36,13 @@ flutter:
|
||||||
|
|
||||||
assets:
|
assets:
|
||||||
- locales/en-US/main.ftl
|
- locales/en-US/main.ftl
|
||||||
|
- locales/de/main.ftl
|
||||||
|
- locales/es/main.ftl
|
||||||
|
- locales/fr/main.ftl
|
||||||
|
- locales/he/main.ftl
|
||||||
|
- locales/ja/main.ftl
|
||||||
|
- locales/pt/main.ftl
|
||||||
|
- locales/ru/main.ftl
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Atkinson Hyperlegible
|
- family: Atkinson Hyperlegible
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,50 @@ void main() {
|
||||||
expect(deps.browser.error.value, isNull);
|
expect(deps.browser.error.value, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('choosing a language changes the interface in place',
|
||||||
|
(tester) async {
|
||||||
|
await pumpApp(tester);
|
||||||
|
await deps.browser.open(MewsUrls.settings);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Settings'), findsOneWidget);
|
||||||
|
|
||||||
|
// The language group sits below the fold.
|
||||||
|
Future<void> scrollTo(String label) async {
|
||||||
|
final target = find.textContaining(label);
|
||||||
|
for (var i = 0; i < 30 && target.evaluate().isEmpty; i++) {
|
||||||
|
await tester.drag(find.byType(ListView), const Offset(0, -200));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
await tester.ensureVisible(target.first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each language is named in its own words, so it can be found from an
|
||||||
|
// interface the reader cannot read.
|
||||||
|
await scrollTo('Deutsch');
|
||||||
|
final stepsBack = deps.browser.canGoBack;
|
||||||
|
await tapRun(tester, 'Deutsch');
|
||||||
|
expect(deps.settings.language.value, 'de');
|
||||||
|
// Applied in place, like the other reader choices.
|
||||||
|
expect(deps.browser.currentPage.value?.url, MewsUrls.settings);
|
||||||
|
expect(deps.browser.canGoBack, stepsBack);
|
||||||
|
expect(find.text('Einstellungen'), findsOneWidget);
|
||||||
|
expect(find.text('Settings'), findsNothing);
|
||||||
|
|
||||||
|
// Hebrew turns the whole page around.
|
||||||
|
await scrollTo('עברית');
|
||||||
|
await tapRun(tester, 'עברית');
|
||||||
|
expect(deps.settings.language.value, 'he');
|
||||||
|
expect(deps.browser.currentPage.value?.direction, TextDirection.rtl);
|
||||||
|
|
||||||
|
// Back to English.
|
||||||
|
await scrollTo('English');
|
||||||
|
await tapRun(tester, 'English');
|
||||||
|
expect(deps.settings.language.value, 'en-US');
|
||||||
|
expect(deps.browser.currentPage.value?.direction, TextDirection.ltr);
|
||||||
|
expect(find.text('Settings'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('bare mews: and an unknown name resolve predictably',
|
testWidgets('bare mews: and an unknown name resolve predictably',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await pumpApp(tester);
|
await pumpApp(tester);
|
||||||
|
|
|
||||||
162
app/test/locales_test.dart
Normal file
162
app/test/locales_test.dart
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:mews/l10n/locales.dart';
|
||||||
|
import 'package:mews/l10n/strings.dart';
|
||||||
|
|
||||||
|
final _message = RegExp(r'^([a-z0-9-]+) = (.*)$', multiLine: true);
|
||||||
|
final _placeholder = RegExp(r'\{ \$([a-z]+) \}');
|
||||||
|
|
||||||
|
Map<String, String> _messagesOf(String tag) {
|
||||||
|
final source = File('locales/$tag/main.ftl').readAsStringSync();
|
||||||
|
return {
|
||||||
|
for (final m in _message.allMatches(source)) m.group(1)!: m.group(2)!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final english = _messagesOf(fallbackLocale);
|
||||||
|
|
||||||
|
test('every shipped language has a file declared as an asset', () {
|
||||||
|
final pubspec = File('pubspec.yaml').readAsStringSync();
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
expect(File('locales/${locale.tag}/main.ftl').existsSync(), isTrue,
|
||||||
|
reason: '${locale.tag} has no messages');
|
||||||
|
expect(pubspec, contains('locales/${locale.tag}/main.ftl'),
|
||||||
|
reason: '${locale.tag} is not bundled as an asset');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every language says everything English says', () {
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
if (locale.tag == fallbackLocale) continue;
|
||||||
|
final messages = _messagesOf(locale.tag);
|
||||||
|
for (final id in english.keys) {
|
||||||
|
expect(messages, contains(id),
|
||||||
|
reason: '${locale.tag} is missing $id');
|
||||||
|
expect(messages[id], isNotEmpty, reason: '${locale.tag}: $id is empty');
|
||||||
|
}
|
||||||
|
// The only messages a language may add are plural forms English has no
|
||||||
|
// use for, such as Russian's few or Hebrew's two.
|
||||||
|
for (final id in messages.keys.where((i) => !english.containsKey(i))) {
|
||||||
|
expect(id, matches(r'-(two|few)$'),
|
||||||
|
reason: '${locale.tag} defines an unknown $id');
|
||||||
|
expect(english, contains(id.replaceFirst(RegExp(r'-(two|few)$'), '-many')),
|
||||||
|
reason: '$id has no English counterpart to fall back on');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every language keeps the placeholders a message needs', () {
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
if (locale.tag == fallbackLocale) continue;
|
||||||
|
final messages = _messagesOf(locale.tag);
|
||||||
|
for (final entry in english.entries) {
|
||||||
|
final wanted = _placeholder
|
||||||
|
.allMatches(entry.value)
|
||||||
|
.map((m) => m.group(1))
|
||||||
|
.toSet();
|
||||||
|
final got = _placeholder
|
||||||
|
.allMatches(messages[entry.key]!)
|
||||||
|
.map((m) => m.group(1))
|
||||||
|
.toSet();
|
||||||
|
// A dropped placeholder loses the number or name it carried.
|
||||||
|
expect(got, containsAll(wanted),
|
||||||
|
reason: '${locale.tag}: ${entry.key} drops a placeholder');
|
||||||
|
// An invented one renders as nothing. The exception is the count on a
|
||||||
|
// singular message: English writes "1 bookmark", but a language whose
|
||||||
|
// singular also covers 21 and 31 has to show the number.
|
||||||
|
final extra = got.difference(wanted);
|
||||||
|
expect(
|
||||||
|
extra.isEmpty ||
|
||||||
|
(extra.length == 1 &&
|
||||||
|
extra.first == 'count' &&
|
||||||
|
entry.key.endsWith('-one')),
|
||||||
|
isTrue,
|
||||||
|
reason: '${locale.tag}: ${entry.key} adds $extra');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a language naming a plural form defines every one it needs', () {
|
||||||
|
// The plural rules decide which suffix a count asks for; a language whose
|
||||||
|
// rules can produce a form it never defines would fall back to English.
|
||||||
|
for (final locale in appLocales) {
|
||||||
|
final messages = _messagesOf(locale.tag);
|
||||||
|
final stems = english.keys
|
||||||
|
.where((id) => id.endsWith('-many'))
|
||||||
|
.map((id) => id.substring(0, id.length - '-many'.length));
|
||||||
|
for (final stem in stems) {
|
||||||
|
for (var count = 1; count <= 200; count++) {
|
||||||
|
final suffix = pluralSuffix(locale.tag, count);
|
||||||
|
if (suffix == 'one' || suffix == 'many') continue;
|
||||||
|
expect(messages, contains('$stem-$suffix'),
|
||||||
|
reason: '${locale.tag} needs $stem-$suffix for $count');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('choosing a language', () {
|
||||||
|
test('matches on the language alone, so a region still fits', () {
|
||||||
|
expect(matchLocale('de-AT'), 'de');
|
||||||
|
expect(matchLocale('pt-BR'), 'pt');
|
||||||
|
expect(matchLocale('en-GB'), 'en-US');
|
||||||
|
expect(matchLocale('en-US'), 'en-US');
|
||||||
|
expect(matchLocale('is'), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('takes the first of the device languages it ships', () {
|
||||||
|
expect(resolveLocale(['is', 'fo', 'ru-RU']), 'ru');
|
||||||
|
expect(resolveLocale(['is', 'fo']), fallbackLocale);
|
||||||
|
expect(resolveLocale([]), fallbackLocale);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('knows which languages mirror', () {
|
||||||
|
expect(isRightToLeft('he'), isTrue);
|
||||||
|
expect(isRightToLeft('en-US'), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('plural rules', () {
|
||||||
|
test('Russian separates two to four from the rest', () {
|
||||||
|
expect(pluralSuffix('ru', 1), 'one');
|
||||||
|
expect(pluralSuffix('ru', 21), 'one');
|
||||||
|
expect(pluralSuffix('ru', 11), 'many');
|
||||||
|
expect(pluralSuffix('ru', 2), 'few');
|
||||||
|
expect(pluralSuffix('ru', 24), 'few');
|
||||||
|
expect(pluralSuffix('ru', 12), 'many');
|
||||||
|
expect(pluralSuffix('ru', 5), 'many');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Hebrew has a form for two', () {
|
||||||
|
expect(pluralSuffix('he', 1), 'one');
|
||||||
|
expect(pluralSuffix('he', 2), 'two');
|
||||||
|
expect(pluralSuffix('he', 3), 'many');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('French and Portuguese read one for a single thing', () {
|
||||||
|
expect(pluralSuffix('fr', 1), 'one');
|
||||||
|
expect(pluralSuffix('fr', 2), 'many');
|
||||||
|
expect(pluralSuffix('pt', 1), 'one');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Japanese has one form', () {
|
||||||
|
expect(pluralSuffix('ja', 1), 'many');
|
||||||
|
expect(pluralSuffix('ja', 7), 'many');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a message missing from a language falls back to English', () {
|
||||||
|
final strings = Strings()
|
||||||
|
..loadSource(File('locales/en-US/main.ftl').readAsStringSync())
|
||||||
|
..loadSource('nav-posts = Beiträge', tag: 'de');
|
||||||
|
|
||||||
|
expect(strings.t('nav-posts'), 'Beiträge');
|
||||||
|
// Not translated in this stub, so English stands in rather than an id.
|
||||||
|
expect(strings.t('nav-bookmarks'), 'Bookmarks');
|
||||||
|
expect(strings.t('no-such-message'), 'no-such-message');
|
||||||
|
expect(strings.lookup('no-such-message'), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue