diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index e03f421761..e765fa4ea0 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -943,6 +943,26 @@
"@openLinksWithInAppBrowser": {
"description": "Label for toggling setting to open links with in-app browser"
},
+ "languageSettingTitle": "Language",
+ "@languageSettingTitle": {
+ "description": "Title for language setting."
+ },
+ "languageEn": "English",
+ "@languageEn": {
+ "description": "Label for the English language."
+ },
+ "languagePl": "Polish",
+ "@languagePl": {
+ "description": "Label for the Polish language."
+ },
+ "languageRu": "Russian",
+ "@languageRu": {
+ "description": "Label for the Russian language."
+ },
+ "languageUk": "Ukrainian",
+ "@languageUk": {
+ "description": "Label for the Ukrainian language."
+ },
"pollWidgetQuestionMissing": "No question.",
"@pollWidgetQuestionMissing": {
"description": "Text to display for a poll when the question is missing"
diff --git a/docs/translation.md b/docs/translation.md
index c8cc132384..a2875b29f5 100644
--- a/docs/translation.md
+++ b/docs/translation.md
@@ -25,6 +25,8 @@ is working correctly.
## Adding new UI strings
+
+
### Adding a string to the translation database
To add a new string in the UI, start by
@@ -79,6 +81,26 @@ For example:
`zulipLocalizations.subscribedToNChannels(store.subscriptions.length)`.
+## Adding a new language
+
+ARB files for new languages are automatically created in pull requests generated
+by [the update-translations GitHub workflow](/.github/workflows/update-translations.yml).
+However, this won't make them in the in-app settings UI.
+
+On [Weblate](https://hosted.weblate.org/projects/zulip/zulip-flutter/),
+we can check the percentage of strings translated in each language.
+We use this information to determine if we should start offerring the language
+in the in-app settings UI. For reference, on the web app, we offer a language
+when it is 5% translated. (Search for `percent_translated` in the server code.)
+
+When a language has a good percentage of strings translated, follow these steps
+to add it:
+
+- If the language tag is, for example, 'en-GB', [add a string](#add-string)
+ named 'languageEnGb'.
+- Update [localizations.dart](/lib/model/localizations.dart) to include the new language in
+ `languages`, following the instructions there.
+
## Hack to enforce locale (for testing, etc.)
For testing the app's behavior in different locales,
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index c13cdd3a9f..26ca909743 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -1407,6 +1407,36 @@ abstract class ZulipLocalizations {
/// **'Open links with in-app browser'**
String get openLinksWithInAppBrowser;
+ /// Title for language setting.
+ ///
+ /// In en, this message translates to:
+ /// **'Language'**
+ String get languageSettingTitle;
+
+ /// Label for the English language.
+ ///
+ /// In en, this message translates to:
+ /// **'English'**
+ String get languageEn;
+
+ /// Label for the Polish language.
+ ///
+ /// In en, this message translates to:
+ /// **'Polish'**
+ String get languagePl;
+
+ /// Label for the Russian language.
+ ///
+ /// In en, this message translates to:
+ /// **'Russian'**
+ String get languageRu;
+
+ /// Label for the Ukrainian language.
+ ///
+ /// In en, this message translates to:
+ /// **'Ukrainian'**
+ String get languageUk;
+
/// Text to display for a poll when the question is missing
///
/// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index c47095ee06..94342640bd 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index 301f70d34d..f148d08418 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 52e4393767..7c775d9bdf 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index cb6dc8a2ce..c019ab07e1 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -773,6 +773,21 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 1fdc2f585d..a4030595f6 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 4bdd16533d..6c3f94658e 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index e046358d11..f8d15dc614 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -777,6 +777,21 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Otwieraj odnośniki w aplikacji';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'Brak pytania.';
diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 57b33f03b1..b9c3081824 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -781,6 +781,21 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Открывать ссылки внутри приложения';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'Нет вопроса.';
diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 29e203cccb..e78d100f30 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -769,6 +769,21 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'Bez otázky.';
diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index c69ff12045..e8055c7320 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -789,6 +789,21 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
String get openLinksWithInAppBrowser =>
'Odpri povezave v brskalniku znotraj aplikacije';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'Brez vprašanja.';
diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index 6c5e7264d1..4278ff3297 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -781,6 +781,21 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
String get openLinksWithInAppBrowser =>
'Відкривати посилання за допомогою браузера додатку';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'Немає питання.';
diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index d61e7cd6e3..f915137801 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -767,6 +767,21 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
@override
String get openLinksWithInAppBrowser => 'Open links with in-app browser';
+ @override
+ String get languageSettingTitle => 'Language';
+
+ @override
+ String get languageEn => 'English';
+
+ @override
+ String get languagePl => 'Polish';
+
+ @override
+ String get languageRu => 'Russian';
+
+ @override
+ String get languageUk => 'Ukrainian';
+
@override
String get pollWidgetQuestionMissing => 'No question.';
diff --git a/lib/model/database.dart b/lib/model/database.dart
index e20380b9e7..0ae04e2928 100644
--- a/lib/model/database.dart
+++ b/lib/model/database.dart
@@ -1,3 +1,5 @@
+import 'dart:ui';
+
import 'package:drift/drift.dart';
import 'package:drift/internal/versioned_schema.dart';
import 'package:drift/remote.dart';
@@ -30,6 +32,9 @@ class GlobalSettings extends Table {
Column get markReadOnScroll => textEnum()
.nullable()();
+ Column get language => text().map(const LocaleConverter())
+ .nullable()();
+
// If adding a new column to this table, consider whether [BoolGlobalSettings]
// can do the job instead (by adding a value to the [BoolGlobalSetting] enum).
// That way is more convenient, when it works, because
@@ -125,7 +130,7 @@ class AppDatabase extends _$AppDatabase {
// information on using the build_runner.
// * Write a migration in `_migrationSteps` below.
// * Write tests.
- static const int latestSchemaVersion = 8; // See note.
+ static const int latestSchemaVersion = 9; // See note.
@override
int get schemaVersion => latestSchemaVersion;
@@ -188,6 +193,9 @@ class AppDatabase extends _$AppDatabase {
await m.addColumn(schema.globalSettings,
schema.globalSettings.markReadOnScroll);
},
+ from8To9: (m, schema) async {
+ await m.addColumn(schema.globalSettings, schema.globalSettings.language);
+ }
);
Future _createLatestSchema(Migrator m) async {
@@ -257,3 +265,55 @@ class AppDatabase extends _$AppDatabase {
}
class AccountAlreadyExistsException implements Exception {}
+
+class LocaleConverter extends TypeConverter {
+ const LocaleConverter();
+
+ /// Parse a Unicode BCP 47 Language Identifier into [Locale].
+ ///
+ /// Throw when it fails to convert [languageTag] into a [Locale].
+ ///
+ /// This supports parsing a Unicode Language Identifier returned from
+ /// [Locale.toLanguageTag].
+ ///
+ /// This implementation refers to a part of
+ /// [this EBNF grammar](https://www.unicode.org/reports/tr35/#Unicode_language_identifier),
+ /// assuming the identifier is valid without
+ /// [unicode_variant_subtag](https://www.unicode.org/reports/tr35/#unicode_variant_subtag).
+ ///
+ /// This doesn't check if the [languageTag] is a valid identifier, (i.e., when
+ /// this returns without errors, the identifier is not necessarily
+ /// syntactically well-formed or valid).
+ // TODO(upstream): send this as a factory Locale.fromLanguageTag
+ // https://github.com/flutter/flutter/issues/143491
+ Locale _fromLanguageTag(String languageTag) {
+ final subtags = languageTag.replaceAll('_', '-').split('-');
+
+ return switch (subtags) {
+ [final language, final script, final region] =>
+ Locale.fromSubtags(
+ languageCode: language, scriptCode: script, countryCode: region),
+
+ [final language, final script] when script.length == 4 =>
+ Locale.fromSubtags(languageCode: language, scriptCode: script),
+
+ [final language, final region] =>
+ Locale(language, region),
+
+ [final language] =>
+ Locale(language),
+
+ _ => throw ArgumentError.value(languageTag, 'languageTag'),
+ };
+ }
+
+ @override
+ Locale fromSql(String fromDb) {
+ return _fromLanguageTag(fromDb);
+ }
+
+ @override
+ String toSql(Locale value) {
+ return value.toLanguageTag();
+ }
+}
diff --git a/lib/model/database.g.dart b/lib/model/database.g.dart
index d78f7ede84..b81844eaad 100644
--- a/lib/model/database.g.dart
+++ b/lib/model/database.g.dart
@@ -57,11 +57,21 @@ class $GlobalSettingsTable extends GlobalSettings
$GlobalSettingsTable.$convertermarkReadOnScrolln,
);
@override
+ late final GeneratedColumnWithTypeConverter language =
+ GeneratedColumn(
+ 'language',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ ).withConverter($GlobalSettingsTable.$converterlanguagen);
+ @override
List get $columns => [
themeSetting,
browserPreference,
visitFirstUnread,
markReadOnScroll,
+ language,
];
@override
String get aliasedName => _alias ?? actualTableName;
@@ -101,6 +111,12 @@ class $GlobalSettingsTable extends GlobalSettings
data['${effectivePrefix}mark_read_on_scroll'],
),
),
+ language: $GlobalSettingsTable.$converterlanguagen.fromSql(
+ attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}language'],
+ ),
+ ),
);
}
@@ -141,6 +157,10 @@ class $GlobalSettingsTable extends GlobalSettings
$convertermarkReadOnScrolln = JsonTypeConverter2.asNullable(
$convertermarkReadOnScroll,
);
+ static TypeConverter $converterlanguage =
+ const LocaleConverter();
+ static TypeConverter $converterlanguagen =
+ NullAwareTypeConverter.wrap($converterlanguage);
}
class GlobalSettingsData extends DataClass
@@ -149,11 +169,13 @@ class GlobalSettingsData extends DataClass
final BrowserPreference? browserPreference;
final VisitFirstUnreadSetting? visitFirstUnread;
final MarkReadOnScrollSetting? markReadOnScroll;
+ final Locale? language;
const GlobalSettingsData({
this.themeSetting,
this.browserPreference,
this.visitFirstUnread,
this.markReadOnScroll,
+ this.language,
});
@override
Map toColumns(bool nullToAbsent) {
@@ -184,6 +206,11 @@ class GlobalSettingsData extends DataClass
),
);
}
+ if (!nullToAbsent || language != null) {
+ map['language'] = Variable(
+ $GlobalSettingsTable.$converterlanguagen.toSql(language),
+ );
+ }
return map;
}
@@ -201,6 +228,9 @@ class GlobalSettingsData extends DataClass
markReadOnScroll: markReadOnScroll == null && nullToAbsent
? const Value.absent()
: Value(markReadOnScroll),
+ language: language == null && nullToAbsent
+ ? const Value.absent()
+ : Value(language),
);
}
@@ -219,6 +249,7 @@ class GlobalSettingsData extends DataClass
.fromJson(serializer.fromJson(json['visitFirstUnread'])),
markReadOnScroll: $GlobalSettingsTable.$convertermarkReadOnScrolln
.fromJson(serializer.fromJson(json['markReadOnScroll'])),
+ language: serializer.fromJson(json['language']),
);
}
@override
@@ -243,6 +274,7 @@ class GlobalSettingsData extends DataClass
markReadOnScroll,
),
),
+ 'language': serializer.toJson(language),
};
}
@@ -251,6 +283,7 @@ class GlobalSettingsData extends DataClass
Value browserPreference = const Value.absent(),
Value visitFirstUnread = const Value.absent(),
Value markReadOnScroll = const Value.absent(),
+ Value language = const Value.absent(),
}) => GlobalSettingsData(
themeSetting: themeSetting.present ? themeSetting.value : this.themeSetting,
browserPreference: browserPreference.present
@@ -262,6 +295,7 @@ class GlobalSettingsData extends DataClass
markReadOnScroll: markReadOnScroll.present
? markReadOnScroll.value
: this.markReadOnScroll,
+ language: language.present ? language.value : this.language,
);
GlobalSettingsData copyWithCompanion(GlobalSettingsCompanion data) {
return GlobalSettingsData(
@@ -277,6 +311,7 @@ class GlobalSettingsData extends DataClass
markReadOnScroll: data.markReadOnScroll.present
? data.markReadOnScroll.value
: this.markReadOnScroll,
+ language: data.language.present ? data.language.value : this.language,
);
}
@@ -286,7 +321,8 @@ class GlobalSettingsData extends DataClass
..write('themeSetting: $themeSetting, ')
..write('browserPreference: $browserPreference, ')
..write('visitFirstUnread: $visitFirstUnread, ')
- ..write('markReadOnScroll: $markReadOnScroll')
+ ..write('markReadOnScroll: $markReadOnScroll, ')
+ ..write('language: $language')
..write(')'))
.toString();
}
@@ -297,6 +333,7 @@ class GlobalSettingsData extends DataClass
browserPreference,
visitFirstUnread,
markReadOnScroll,
+ language,
);
@override
bool operator ==(Object other) =>
@@ -305,7 +342,8 @@ class GlobalSettingsData extends DataClass
other.themeSetting == this.themeSetting &&
other.browserPreference == this.browserPreference &&
other.visitFirstUnread == this.visitFirstUnread &&
- other.markReadOnScroll == this.markReadOnScroll);
+ other.markReadOnScroll == this.markReadOnScroll &&
+ other.language == this.language);
}
class GlobalSettingsCompanion extends UpdateCompanion {
@@ -313,12 +351,14 @@ class GlobalSettingsCompanion extends UpdateCompanion {
final Value browserPreference;
final Value visitFirstUnread;
final Value markReadOnScroll;
+ final Value language;
final Value rowid;
const GlobalSettingsCompanion({
this.themeSetting = const Value.absent(),
this.browserPreference = const Value.absent(),
this.visitFirstUnread = const Value.absent(),
this.markReadOnScroll = const Value.absent(),
+ this.language = const Value.absent(),
this.rowid = const Value.absent(),
});
GlobalSettingsCompanion.insert({
@@ -326,6 +366,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
this.browserPreference = const Value.absent(),
this.visitFirstUnread = const Value.absent(),
this.markReadOnScroll = const Value.absent(),
+ this.language = const Value.absent(),
this.rowid = const Value.absent(),
});
static Insertable custom({
@@ -333,6 +374,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
Expression? browserPreference,
Expression? visitFirstUnread,
Expression? markReadOnScroll,
+ Expression? language,
Expression? rowid,
}) {
return RawValuesInsertable({
@@ -340,6 +382,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
if (browserPreference != null) 'browser_preference': browserPreference,
if (visitFirstUnread != null) 'visit_first_unread': visitFirstUnread,
if (markReadOnScroll != null) 'mark_read_on_scroll': markReadOnScroll,
+ if (language != null) 'language': language,
if (rowid != null) 'rowid': rowid,
});
}
@@ -349,6 +392,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
Value? browserPreference,
Value? visitFirstUnread,
Value? markReadOnScroll,
+ Value? language,
Value? rowid,
}) {
return GlobalSettingsCompanion(
@@ -356,6 +400,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
browserPreference: browserPreference ?? this.browserPreference,
visitFirstUnread: visitFirstUnread ?? this.visitFirstUnread,
markReadOnScroll: markReadOnScroll ?? this.markReadOnScroll,
+ language: language ?? this.language,
rowid: rowid ?? this.rowid,
);
}
@@ -389,6 +434,11 @@ class GlobalSettingsCompanion extends UpdateCompanion {
),
);
}
+ if (language.present) {
+ map['language'] = Variable(
+ $GlobalSettingsTable.$converterlanguagen.toSql(language.value),
+ );
+ }
if (rowid.present) {
map['rowid'] = Variable(rowid.value);
}
@@ -402,6 +452,7 @@ class GlobalSettingsCompanion extends UpdateCompanion {
..write('browserPreference: $browserPreference, ')
..write('visitFirstUnread: $visitFirstUnread, ')
..write('markReadOnScroll: $markReadOnScroll, ')
+ ..write('language: $language, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -1248,6 +1299,7 @@ typedef $$GlobalSettingsTableCreateCompanionBuilder =
Value browserPreference,
Value visitFirstUnread,
Value markReadOnScroll,
+ Value language,
Value rowid,
});
typedef $$GlobalSettingsTableUpdateCompanionBuilder =
@@ -1256,6 +1308,7 @@ typedef $$GlobalSettingsTableUpdateCompanionBuilder =
Value browserPreference,
Value visitFirstUnread,
Value markReadOnScroll,
+ Value language,
Value rowid,
});
@@ -1299,6 +1352,12 @@ class $$GlobalSettingsTableFilterComposer
column: $table.markReadOnScroll,
builder: (column) => ColumnWithTypeConverterFilters(column),
);
+
+ ColumnWithTypeConverterFilters get language =>
+ $composableBuilder(
+ column: $table.language,
+ builder: (column) => ColumnWithTypeConverterFilters(column),
+ );
}
class $$GlobalSettingsTableOrderingComposer
@@ -1329,6 +1388,11 @@ class $$GlobalSettingsTableOrderingComposer
column: $table.markReadOnScroll,
builder: (column) => ColumnOrderings(column),
);
+
+ ColumnOrderings get language => $composableBuilder(
+ column: $table.language,
+ builder: (column) => ColumnOrderings(column),
+ );
}
class $$GlobalSettingsTableAnnotationComposer
@@ -1363,6 +1427,9 @@ class $$GlobalSettingsTableAnnotationComposer
column: $table.markReadOnScroll,
builder: (column) => column,
);
+
+ GeneratedColumnWithTypeConverter get language =>
+ $composableBuilder(column: $table.language, builder: (column) => column);
}
class $$GlobalSettingsTableTableManager
@@ -1409,12 +1476,14 @@ class $$GlobalSettingsTableTableManager
const Value.absent(),
Value markReadOnScroll =
const Value.absent(),
+ Value language = const Value.absent(),
Value rowid = const Value.absent(),
}) => GlobalSettingsCompanion(
themeSetting: themeSetting,
browserPreference: browserPreference,
visitFirstUnread: visitFirstUnread,
markReadOnScroll: markReadOnScroll,
+ language: language,
rowid: rowid,
),
createCompanionCallback:
@@ -1426,12 +1495,14 @@ class $$GlobalSettingsTableTableManager
const Value.absent(),
Value markReadOnScroll =
const Value.absent(),
+ Value language = const Value.absent(),
Value rowid = const Value.absent(),
}) => GlobalSettingsCompanion.insert(
themeSetting: themeSetting,
browserPreference: browserPreference,
visitFirstUnread: visitFirstUnread,
markReadOnScroll: markReadOnScroll,
+ language: language,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
diff --git a/lib/model/localizations.dart b/lib/model/localizations.dart
index 07598cd8fe..1f3391b663 100644
--- a/lib/model/localizations.dart
+++ b/lib/model/localizations.dart
@@ -1,3 +1,7 @@
+import 'dart:ui';
+
+import 'package:collection/collection.dart';
+
import '../generated/l10n/zulip_localizations.dart';
abstract final class GlobalLocalizations {
@@ -15,3 +19,62 @@ abstract final class GlobalLocalizations {
static ZulipLocalizations zulipLocalizations =
lookupZulipLocalizations(ZulipLocalizations.supportedLocales.first);
}
+
+extension ZulipLocalizationsHelper on ZulipLocalizations {
+ /// Returns a list of the locales we offer, with their display name in their
+ /// own locale a.k.a. selfname, and the display name localized in this
+ /// [ZulipLocalizations] instance.
+ ///
+ /// This list only includes some of [ZulipLocalizations.supportedLocales];
+ /// it includes languages that have substantially complete translations.
+ /// For what counts as substantially translated, see docs/translation.md.
+ ///
+ /// The list should be sorted by selfname, to help users find their
+ /// language in the UI. When in doubt how to sort (like between different
+ /// scripts, or in scripts you don't know), try to match the order found in
+ /// other UIs, like for choosing a language in your phone's system settings.
+ ///
+ /// For the values of selfname, consult Wikipedia:
+ /// https://meta.wikimedia.org/wiki/List_of_Wikipedias
+ /// https://en.wikipedia.org/wiki/Special:Preferences
+ /// or better yet, Wikipedia's own mobile UIs. Wikipedia is a very
+ /// conscientiously international and intercultural project with a lot of
+ /// effort going into it by speakers of many languages, which makes it a
+ /// useful gold standard for this.
+ ///
+ /// This is adapted from:
+ /// https://github.com/zulip/zulip-mobile/blob/91f5c3289/src/settings/languages.js
+ ///
+ /// See also:
+ /// * [kSelfnamesByLocale], a map for looking up the selfname of a locale,
+ /// when its [ZulipLocalizations]-specific displayName is not needed.
+ List languages() {
+ return [
+ (Locale('en'), 'English', languageEn),
+ (Locale('pl'), 'Polski', languagePl),
+ (Locale('ru'), 'Русский', languageRu),
+ (Locale('uk'), 'Українська', languageUk),
+ ];
+ }
+}
+
+typedef Language = (Locale locale, String selfname, String displayName);
+
+/// The map, of the locales we offer, to their display name in their own
+/// locale a.k.a. selfname.
+///
+/// See also:
+/// * [ZulipLocalizations.languages], similar helper that returns
+/// a list of locales with their localized display name in that
+/// [ZulipLocalizations] instance and their selfname.
+final kSelfnamesByLocale = UnmodifiableMapView(_selfnamesByLocale);
+final _selfnamesByLocale = {
+ // While [ZulipLocalizations.languages]' result will be different depending
+ // on the language setting, `locale` and `selfname` are the same for all
+ // languages. This makes it fine to generate this map from
+ // [GlobalLocalizations.zulipLocalizations],
+ // despite it being a mutable static field.
+ for (final (locale, selfname, _)
+ in GlobalLocalizations.zulipLocalizations.languages())
+ locale: selfname,
+};
diff --git a/lib/model/schema_versions.g.dart b/lib/model/schema_versions.g.dart
index 5712a94fbb..56be3dcd8d 100644
--- a/lib/model/schema_versions.g.dart
+++ b/lib/model/schema_versions.g.dart
@@ -517,6 +517,84 @@ i1.GeneratedColumn _column_14(String aliasedName) =>
true,
type: i1.DriftSqlType.string,
);
+
+final class Schema9 extends i0.VersionedSchema {
+ Schema9({required super.database}) : super(version: 9);
+ @override
+ late final List entities = [
+ globalSettings,
+ boolGlobalSettings,
+ accounts,
+ ];
+ late final Shape6 globalSettings = Shape6(
+ source: i0.VersionedTable(
+ entityName: 'global_settings',
+ withoutRowId: false,
+ isStrict: false,
+ tableConstraints: [],
+ columns: [_column_9, _column_10, _column_13, _column_14, _column_15],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape3 boolGlobalSettings = Shape3(
+ source: i0.VersionedTable(
+ entityName: 'bool_global_settings',
+ withoutRowId: false,
+ isStrict: false,
+ tableConstraints: ['PRIMARY KEY(name)'],
+ columns: [_column_11, _column_12],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape0 accounts = Shape0(
+ source: i0.VersionedTable(
+ entityName: 'accounts',
+ withoutRowId: false,
+ isStrict: false,
+ tableConstraints: [
+ 'UNIQUE(realm_url, user_id)',
+ 'UNIQUE(realm_url, email)',
+ ],
+ columns: [
+ _column_0,
+ _column_1,
+ _column_2,
+ _column_3,
+ _column_4,
+ _column_5,
+ _column_6,
+ _column_7,
+ _column_8,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+}
+
+class Shape6 extends i0.VersionedTable {
+ Shape6({required super.source, required super.alias}) : super.aliased();
+ i1.GeneratedColumn get themeSetting =>
+ columnsByName['theme_setting']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get browserPreference =>
+ columnsByName['browser_preference']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get visitFirstUnread =>
+ columnsByName['visit_first_unread']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get markReadOnScroll =>
+ columnsByName['mark_read_on_scroll']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get language =>
+ columnsByName['language']! as i1.GeneratedColumn;
+}
+
+i1.GeneratedColumn _column_15(String aliasedName) =>
+ i1.GeneratedColumn(
+ 'language',
+ aliasedName,
+ true,
+ type: i1.DriftSqlType.string,
+ );
i0.MigrationStepWithVersion migrationSteps({
required Future Function(i1.Migrator m, Schema2 schema) from1To2,
required Future Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -525,6 +603,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future Function(i1.Migrator m, Schema6 schema) from5To6,
required Future Function(i1.Migrator m, Schema7 schema) from6To7,
required Future Function(i1.Migrator m, Schema8 schema) from7To8,
+ required Future Function(i1.Migrator m, Schema9 schema) from8To9,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -563,6 +642,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema);
return 8;
+ case 8:
+ final schema = Schema9(database: database);
+ final migrator = i1.Migrator(database, schema);
+ await from8To9(migrator, schema);
+ return 9;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -577,6 +661,7 @@ i1.OnUpgrade stepByStep({
required Future Function(i1.Migrator m, Schema6 schema) from5To6,
required Future Function(i1.Migrator m, Schema7 schema) from6To7,
required Future Function(i1.Migrator m, Schema8 schema) from7To8,
+ required Future Function(i1.Migrator m, Schema9 schema) from8To9,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -586,5 +671,6 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6,
from6To7: from6To7,
from7To8: from7To8,
+ from8To9: from8To9,
),
);
diff --git a/lib/model/settings.dart b/lib/model/settings.dart
index 298980a395..e401b830b7 100644
--- a/lib/model/settings.dart
+++ b/lib/model/settings.dart
@@ -1,3 +1,5 @@
+import 'dart:ui';
+
import 'package:flutter/foundation.dart';
import '../generated/l10n/zulip_localizations.dart';
@@ -324,6 +326,23 @@ class GlobalSettingsStore extends ChangeNotifier {
};
}
+ /// The user's choice of [Locale].
+ ///
+ /// In most cases, this will be present in the list returned by
+ /// [ZulipLocalizations.languages] (i.e., one of our supported languages).
+ /// This might not be true if we migrate from a [Locale] to another for the
+ /// same language.
+ ///
+ /// This is null if the value has never been set.
+ // TODO check if this [Locale] is supported, and update it in the persistent
+ // store if this is known to be an alias of a supported [Locale].
+ Locale? get language => _data.language;
+
+ /// Set [language], persistently for future runs of the app.
+ Future setLanguage(Locale value) async {
+ await _update(GlobalSettingsCompanion(language: Value(value)));
+ }
+
/// The user's choice of the given bool-valued setting, or our default for it.
///
/// See also [setBool].
diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart
index b1aa763ac8..5fced7cb8b 100644
--- a/lib/widgets/app.dart
+++ b/lib/widgets/app.dart
@@ -242,6 +242,7 @@ class _ZulipAppState extends State with WidgetsBindingObserver {
onGenerateTitle: (BuildContext context) {
return ZulipLocalizations.of(context).zulipAppTitle;
},
+ locale: GlobalStoreWidget.settingsOf(context).language,
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
// The context has to be taken from the [Builder] because
diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart
index 394415a8be..4c97a536ac 100644
--- a/lib/widgets/settings.dart
+++ b/lib/widgets/settings.dart
@@ -1,8 +1,12 @@
+import 'dart:async';
+
import 'package:flutter/material.dart';
import '../generated/l10n/zulip_localizations.dart';
+import '../model/localizations.dart';
import '../model/settings.dart';
import 'app_bar.dart';
+import 'icons.dart';
import 'page.dart';
import 'store.dart';
@@ -20,17 +24,18 @@ class SettingsPage extends StatelessWidget {
return Scaffold(
appBar: ZulipAppBar(
title: Text(zulipLocalizations.settingsPageTitle)),
- body: Column(children: [
+ body: SingleChildScrollView(child: Column(children: [
const _ThemeSetting(),
const _BrowserPreferenceSetting(),
const _VisitFirstUnreadSetting(),
const _MarkReadOnScrollSetting(),
+ const _LanguageSetting(),
if (GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty)
ListTile(
title: Text(zulipLocalizations.experimentalFeatureSettingsPageTitle),
onTap: () => Navigator.push(context,
ExperimentalFeaturesPage.buildRoute()))
- ]));
+ ])));
}
}
@@ -231,6 +236,76 @@ class MarkReadOnScrollSettingPage extends StatelessWidget {
}
}
+class _LanguageSetting extends StatelessWidget {
+ const _LanguageSetting();
+
+ @override
+ Widget build(BuildContext context) {
+ final zulipLocalizations = ZulipLocalizations.of(context);
+
+ Widget? subtitle;
+ final currentLanguageSelfname = kSelfnamesByLocale[
+ GlobalStoreWidget.settingsOf(context).language];
+ if (currentLanguageSelfname != null) {
+ subtitle = Text(currentLanguageSelfname);
+ }
+
+ return ListTile(
+ title: Text(zulipLocalizations.languageSettingTitle),
+ subtitle: subtitle,
+ onTap: () => Navigator.push(context, _LanguagePage.buildRoute()));
+ }
+}
+
+class _LanguagePage extends StatelessWidget {
+ const _LanguagePage();
+
+ static WidgetRoute buildRoute() {
+ return MaterialWidgetRoute(page: const _LanguagePage());
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final zulipLocalizations = ZulipLocalizations.of(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(zulipLocalizations.languageSettingTitle)),
+ body: SingleChildScrollView(
+ child: Column(children: [
+ for (final language in zulipLocalizations.languages())
+ _LanguageItem(language: language),
+ ])));
+ }
+}
+
+class _LanguageItem extends StatelessWidget {
+ const _LanguageItem({required this.language});
+
+ /// The [Language] this corresponds to, from [ZulipLocalizations.languages].
+ final Language language;
+
+ @override
+ Widget build(BuildContext context) {
+ final (locale, selfname, displayName) = language;
+ final isCurrentLanguageInSettings =
+ locale == GlobalStoreWidget.settingsOf(context).language;
+
+ return ListTile(
+ title: Text(selfname),
+ subtitle: Text(
+ isCurrentLanguageInSettings
+ ? // Make sure the subtitle text is consistent to the title — since
+ // displayName (decided by translators) can be different from our
+ // hard-coded selfname when isCurrentLanguage is true.
+ selfname
+ : displayName),
+ trailing: isCurrentLanguageInSettings ? Icon(ZulipIcons.check) : null,
+ onTap: () {
+ unawaited(GlobalStoreWidget.settingsOf(context).setLanguage(locale));
+ });
+ }
+}
+
class ExperimentalFeaturesPage extends StatelessWidget {
const ExperimentalFeaturesPage({super.key});
diff --git a/test/model/database_test.dart b/test/model/database_test.dart
index e6e2b729be..2066da2b2d 100644
--- a/test/model/database_test.dart
+++ b/test/model/database_test.dart
@@ -1,3 +1,5 @@
+import 'dart:ui';
+
import 'package:checks/checks.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
@@ -46,6 +48,20 @@ void main() {
check(await db.getGlobalSettings()).themeSetting.equals(ThemeSetting.dark);
});
+ test('LocaleConverter roundtrips', () async {
+ Future doCheck(Locale locale) async {
+ await db.update(db.globalSettings)
+ .write(GlobalSettingsCompanion(language: Value(locale)));
+ check(await db.getGlobalSettings()).language.equals(locale);
+ }
+
+ await doCheck(Locale('en'));
+ await doCheck(Locale('en', 'GB'));
+ await doCheck(Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'));
+ await doCheck(Locale.fromSubtags(
+ languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'));
+ });
+
test('BoolGlobalSettings get ignores unknown names', () async {
await db.into(db.boolGlobalSettings)
.insert(BoolGlobalSettingRow(name: 'nonsense', value: true));
diff --git a/test/model/localizations_test.dart b/test/model/localizations_test.dart
new file mode 100644
index 0000000000..be1e9f6d3f
--- /dev/null
+++ b/test/model/localizations_test.dart
@@ -0,0 +1,20 @@
+import 'package:checks/checks.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:zulip/generated/l10n/zulip_localizations.dart';
+import 'package:zulip/model/localizations.dart';
+
+void main() {
+ test('kSelfnamesByLocale', () {
+ for (final locale in kSelfnamesByLocale.keys) {
+ check(ZulipLocalizations.supportedLocales).contains(locale);
+ }
+ });
+
+ test('languages', () {
+ final zulipLocalizations = GlobalLocalizations.zulipLocalizations;
+ for (final (locale, selfname, _) in zulipLocalizations.languages()) {
+ check(ZulipLocalizations.supportedLocales).contains(locale);
+ check(kSelfnamesByLocale)[locale].isNotNull().equals(selfname);
+ }
+ });
+}
diff --git a/test/model/schemas/drift_schema_v9.json b/test/model/schemas/drift_schema_v9.json
new file mode 100644
index 0000000000..97859dbbd5
--- /dev/null
+++ b/test/model/schemas/drift_schema_v9.json
@@ -0,0 +1 @@
+{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"global_settings","was_declared_in_moor":false,"columns":[{"name":"theme_setting","getter_name":"themeSetting","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeSetting.values)","dart_type_name":"ThemeSetting"}},{"name":"browser_preference","getter_name":"browserPreference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BrowserPreference.values)","dart_type_name":"BrowserPreference"}},{"name":"visit_first_unread","getter_name":"visitFirstUnread","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(VisitFirstUnreadSetting.values)","dart_type_name":"VisitFirstUnreadSetting"}},{"name":"mark_read_on_scroll","getter_name":"markReadOnScroll","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(MarkReadOnScrollSetting.values)","dart_type_name":"MarkReadOnScrollSetting"}},{"name":"language","getter_name":"language","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"bool_global_settings","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"value","getter_name":"value","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"value\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"value\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["name"]}},{"id":2,"references":[],"type":"table","data":{"name":"accounts","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"realm_url","getter_name":"realmUrl","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const UriConverter()","dart_type_name":"Uri"}},{"name":"user_id","getter_name":"userId","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_key","getter_name":"apiKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_version","getter_name":"zulipVersion","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_merge_base","getter_name":"zulipMergeBase","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"zulip_feature_level","getter_name":"zulipFeatureLevel","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"acked_push_token","getter_name":"ackedPushToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"unique_keys":[["realm_url","user_id"],["realm_url","email"]]}}]}
\ No newline at end of file
diff --git a/test/model/schemas/schema.dart b/test/model/schemas/schema.dart
index 746206e453..413b4408c4 100644
--- a/test/model/schemas/schema.dart
+++ b/test/model/schemas/schema.dart
@@ -11,6 +11,7 @@ import 'schema_v5.dart' as v5;
import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8;
+import 'schema_v9.dart' as v9;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -32,10 +33,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v7.DatabaseAtV7(db);
case 8:
return v8.DatabaseAtV8(db);
+ case 9:
+ return v9.DatabaseAtV9(db);
default:
throw MissingSchemaException(version, versions);
}
}
- static const versions = const [1, 2, 3, 4, 5, 6, 7, 8];
+ static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9];
}
diff --git a/test/model/schemas/schema_v9.dart b/test/model/schemas/schema_v9.dart
new file mode 100644
index 0000000000..d35d417cd9
--- /dev/null
+++ b/test/model/schemas/schema_v9.dart
@@ -0,0 +1,1006 @@
+// dart format width=80
+// GENERATED CODE, DO NOT EDIT BY HAND.
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart';
+
+class GlobalSettings extends Table
+ with TableInfo {
+ @override
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ GlobalSettings(this.attachedDatabase, [this._alias]);
+ late final GeneratedColumn themeSetting = GeneratedColumn(
+ 'theme_setting',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ late final GeneratedColumn browserPreference =
+ GeneratedColumn(
+ 'browser_preference',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ late final GeneratedColumn visitFirstUnread = GeneratedColumn(
+ 'visit_first_unread',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ late final GeneratedColumn markReadOnScroll = GeneratedColumn(
+ 'mark_read_on_scroll',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ late final GeneratedColumn language = GeneratedColumn(
+ 'language',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ @override
+ List get $columns => [
+ themeSetting,
+ browserPreference,
+ visitFirstUnread,
+ markReadOnScroll,
+ language,
+ ];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'global_settings';
+ @override
+ Set get $primaryKey => const {};
+ @override
+ GlobalSettingsData map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return GlobalSettingsData(
+ themeSetting: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}theme_setting'],
+ ),
+ browserPreference: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}browser_preference'],
+ ),
+ visitFirstUnread: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}visit_first_unread'],
+ ),
+ markReadOnScroll: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}mark_read_on_scroll'],
+ ),
+ language: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}language'],
+ ),
+ );
+ }
+
+ @override
+ GlobalSettings createAlias(String alias) {
+ return GlobalSettings(attachedDatabase, alias);
+ }
+}
+
+class GlobalSettingsData extends DataClass
+ implements Insertable {
+ final String? themeSetting;
+ final String? browserPreference;
+ final String? visitFirstUnread;
+ final String? markReadOnScroll;
+ final String? language;
+ const GlobalSettingsData({
+ this.themeSetting,
+ this.browserPreference,
+ this.visitFirstUnread,
+ this.markReadOnScroll,
+ this.language,
+ });
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (!nullToAbsent || themeSetting != null) {
+ map['theme_setting'] = Variable(themeSetting);
+ }
+ if (!nullToAbsent || browserPreference != null) {
+ map['browser_preference'] = Variable(browserPreference);
+ }
+ if (!nullToAbsent || visitFirstUnread != null) {
+ map['visit_first_unread'] = Variable(visitFirstUnread);
+ }
+ if (!nullToAbsent || markReadOnScroll != null) {
+ map['mark_read_on_scroll'] = Variable(markReadOnScroll);
+ }
+ if (!nullToAbsent || language != null) {
+ map['language'] = Variable(language);
+ }
+ return map;
+ }
+
+ GlobalSettingsCompanion toCompanion(bool nullToAbsent) {
+ return GlobalSettingsCompanion(
+ themeSetting: themeSetting == null && nullToAbsent
+ ? const Value.absent()
+ : Value(themeSetting),
+ browserPreference: browserPreference == null && nullToAbsent
+ ? const Value.absent()
+ : Value(browserPreference),
+ visitFirstUnread: visitFirstUnread == null && nullToAbsent
+ ? const Value.absent()
+ : Value(visitFirstUnread),
+ markReadOnScroll: markReadOnScroll == null && nullToAbsent
+ ? const Value.absent()
+ : Value(markReadOnScroll),
+ language: language == null && nullToAbsent
+ ? const Value.absent()
+ : Value(language),
+ );
+ }
+
+ factory GlobalSettingsData.fromJson(
+ Map json, {
+ ValueSerializer? serializer,
+ }) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return GlobalSettingsData(
+ themeSetting: serializer.fromJson(json['themeSetting']),
+ browserPreference: serializer.fromJson(
+ json['browserPreference'],
+ ),
+ visitFirstUnread: serializer.fromJson(json['visitFirstUnread']),
+ markReadOnScroll: serializer.fromJson(json['markReadOnScroll']),
+ language: serializer.fromJson(json['language']),
+ );
+ }
+ @override
+ Map toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return {
+ 'themeSetting': serializer.toJson(themeSetting),
+ 'browserPreference': serializer.toJson(browserPreference),
+ 'visitFirstUnread': serializer.toJson(visitFirstUnread),
+ 'markReadOnScroll': serializer.toJson(markReadOnScroll),
+ 'language': serializer.toJson(language),
+ };
+ }
+
+ GlobalSettingsData copyWith({
+ Value themeSetting = const Value.absent(),
+ Value browserPreference = const Value.absent(),
+ Value visitFirstUnread = const Value.absent(),
+ Value markReadOnScroll = const Value.absent(),
+ Value language = const Value.absent(),
+ }) => GlobalSettingsData(
+ themeSetting: themeSetting.present ? themeSetting.value : this.themeSetting,
+ browserPreference: browserPreference.present
+ ? browserPreference.value
+ : this.browserPreference,
+ visitFirstUnread: visitFirstUnread.present
+ ? visitFirstUnread.value
+ : this.visitFirstUnread,
+ markReadOnScroll: markReadOnScroll.present
+ ? markReadOnScroll.value
+ : this.markReadOnScroll,
+ language: language.present ? language.value : this.language,
+ );
+ GlobalSettingsData copyWithCompanion(GlobalSettingsCompanion data) {
+ return GlobalSettingsData(
+ themeSetting: data.themeSetting.present
+ ? data.themeSetting.value
+ : this.themeSetting,
+ browserPreference: data.browserPreference.present
+ ? data.browserPreference.value
+ : this.browserPreference,
+ visitFirstUnread: data.visitFirstUnread.present
+ ? data.visitFirstUnread.value
+ : this.visitFirstUnread,
+ markReadOnScroll: data.markReadOnScroll.present
+ ? data.markReadOnScroll.value
+ : this.markReadOnScroll,
+ language: data.language.present ? data.language.value : this.language,
+ );
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('GlobalSettingsData(')
+ ..write('themeSetting: $themeSetting, ')
+ ..write('browserPreference: $browserPreference, ')
+ ..write('visitFirstUnread: $visitFirstUnread, ')
+ ..write('markReadOnScroll: $markReadOnScroll, ')
+ ..write('language: $language')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ themeSetting,
+ browserPreference,
+ visitFirstUnread,
+ markReadOnScroll,
+ language,
+ );
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is GlobalSettingsData &&
+ other.themeSetting == this.themeSetting &&
+ other.browserPreference == this.browserPreference &&
+ other.visitFirstUnread == this.visitFirstUnread &&
+ other.markReadOnScroll == this.markReadOnScroll &&
+ other.language == this.language);
+}
+
+class GlobalSettingsCompanion extends UpdateCompanion {
+ final Value themeSetting;
+ final Value browserPreference;
+ final Value visitFirstUnread;
+ final Value markReadOnScroll;
+ final Value language;
+ final Value rowid;
+ const GlobalSettingsCompanion({
+ this.themeSetting = const Value.absent(),
+ this.browserPreference = const Value.absent(),
+ this.visitFirstUnread = const Value.absent(),
+ this.markReadOnScroll = const Value.absent(),
+ this.language = const Value.absent(),
+ this.rowid = const Value.absent(),
+ });
+ GlobalSettingsCompanion.insert({
+ this.themeSetting = const Value.absent(),
+ this.browserPreference = const Value.absent(),
+ this.visitFirstUnread = const Value.absent(),
+ this.markReadOnScroll = const Value.absent(),
+ this.language = const Value.absent(),
+ this.rowid = const Value.absent(),
+ });
+ static Insertable custom({
+ Expression? themeSetting,
+ Expression? browserPreference,
+ Expression? visitFirstUnread,
+ Expression? markReadOnScroll,
+ Expression? language,
+ Expression? rowid,
+ }) {
+ return RawValuesInsertable({
+ if (themeSetting != null) 'theme_setting': themeSetting,
+ if (browserPreference != null) 'browser_preference': browserPreference,
+ if (visitFirstUnread != null) 'visit_first_unread': visitFirstUnread,
+ if (markReadOnScroll != null) 'mark_read_on_scroll': markReadOnScroll,
+ if (language != null) 'language': language,
+ if (rowid != null) 'rowid': rowid,
+ });
+ }
+
+ GlobalSettingsCompanion copyWith({
+ Value? themeSetting,
+ Value? browserPreference,
+ Value? visitFirstUnread,
+ Value? markReadOnScroll,
+ Value? language,
+ Value? rowid,
+ }) {
+ return GlobalSettingsCompanion(
+ themeSetting: themeSetting ?? this.themeSetting,
+ browserPreference: browserPreference ?? this.browserPreference,
+ visitFirstUnread: visitFirstUnread ?? this.visitFirstUnread,
+ markReadOnScroll: markReadOnScroll ?? this.markReadOnScroll,
+ language: language ?? this.language,
+ rowid: rowid ?? this.rowid,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (themeSetting.present) {
+ map['theme_setting'] = Variable(themeSetting.value);
+ }
+ if (browserPreference.present) {
+ map['browser_preference'] = Variable(browserPreference.value);
+ }
+ if (visitFirstUnread.present) {
+ map['visit_first_unread'] = Variable(visitFirstUnread.value);
+ }
+ if (markReadOnScroll.present) {
+ map['mark_read_on_scroll'] = Variable(markReadOnScroll.value);
+ }
+ if (language.present) {
+ map['language'] = Variable(language.value);
+ }
+ if (rowid.present) {
+ map['rowid'] = Variable(rowid.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('GlobalSettingsCompanion(')
+ ..write('themeSetting: $themeSetting, ')
+ ..write('browserPreference: $browserPreference, ')
+ ..write('visitFirstUnread: $visitFirstUnread, ')
+ ..write('markReadOnScroll: $markReadOnScroll, ')
+ ..write('language: $language, ')
+ ..write('rowid: $rowid')
+ ..write(')'))
+ .toString();
+ }
+}
+
+class BoolGlobalSettings extends Table
+ with TableInfo {
+ @override
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ BoolGlobalSettings(this.attachedDatabase, [this._alias]);
+ late final GeneratedColumn name = GeneratedColumn(
+ 'name',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn value = GeneratedColumn(
+ 'value',
+ aliasedName,
+ false,
+ type: DriftSqlType.bool,
+ requiredDuringInsert: true,
+ defaultConstraints: GeneratedColumn.constraintIsAlways(
+ 'CHECK ("value" IN (0, 1))',
+ ),
+ );
+ @override
+ List get $columns => [name, value];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'bool_global_settings';
+ @override
+ Set get $primaryKey => {name};
+ @override
+ BoolGlobalSettingsData map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return BoolGlobalSettingsData(
+ name: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}name'],
+ )!,
+ value: attachedDatabase.typeMapping.read(
+ DriftSqlType.bool,
+ data['${effectivePrefix}value'],
+ )!,
+ );
+ }
+
+ @override
+ BoolGlobalSettings createAlias(String alias) {
+ return BoolGlobalSettings(attachedDatabase, alias);
+ }
+}
+
+class BoolGlobalSettingsData extends DataClass
+ implements Insertable {
+ final String name;
+ final bool value;
+ const BoolGlobalSettingsData({required this.name, required this.value});
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['name'] = Variable(name);
+ map['value'] = Variable(value);
+ return map;
+ }
+
+ BoolGlobalSettingsCompanion toCompanion(bool nullToAbsent) {
+ return BoolGlobalSettingsCompanion(name: Value(name), value: Value(value));
+ }
+
+ factory BoolGlobalSettingsData.fromJson(
+ Map json, {
+ ValueSerializer? serializer,
+ }) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return BoolGlobalSettingsData(
+ name: serializer.fromJson(json['name']),
+ value: serializer.fromJson(json['value']),
+ );
+ }
+ @override
+ Map toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return {
+ 'name': serializer.toJson(name),
+ 'value': serializer.toJson(value),
+ };
+ }
+
+ BoolGlobalSettingsData copyWith({String? name, bool? value}) =>
+ BoolGlobalSettingsData(
+ name: name ?? this.name,
+ value: value ?? this.value,
+ );
+ BoolGlobalSettingsData copyWithCompanion(BoolGlobalSettingsCompanion data) {
+ return BoolGlobalSettingsData(
+ name: data.name.present ? data.name.value : this.name,
+ value: data.value.present ? data.value.value : this.value,
+ );
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('BoolGlobalSettingsData(')
+ ..write('name: $name, ')
+ ..write('value: $value')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(name, value);
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is BoolGlobalSettingsData &&
+ other.name == this.name &&
+ other.value == this.value);
+}
+
+class BoolGlobalSettingsCompanion
+ extends UpdateCompanion {
+ final Value name;
+ final Value value;
+ final Value rowid;
+ const BoolGlobalSettingsCompanion({
+ this.name = const Value.absent(),
+ this.value = const Value.absent(),
+ this.rowid = const Value.absent(),
+ });
+ BoolGlobalSettingsCompanion.insert({
+ required String name,
+ required bool value,
+ this.rowid = const Value.absent(),
+ }) : name = Value(name),
+ value = Value(value);
+ static Insertable custom({
+ Expression? name,
+ Expression? value,
+ Expression? rowid,
+ }) {
+ return RawValuesInsertable({
+ if (name != null) 'name': name,
+ if (value != null) 'value': value,
+ if (rowid != null) 'rowid': rowid,
+ });
+ }
+
+ BoolGlobalSettingsCompanion copyWith({
+ Value? name,
+ Value? value,
+ Value? rowid,
+ }) {
+ return BoolGlobalSettingsCompanion(
+ name: name ?? this.name,
+ value: value ?? this.value,
+ rowid: rowid ?? this.rowid,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (name.present) {
+ map['name'] = Variable(name.value);
+ }
+ if (value.present) {
+ map['value'] = Variable(value.value);
+ }
+ if (rowid.present) {
+ map['rowid'] = Variable(rowid.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('BoolGlobalSettingsCompanion(')
+ ..write('name: $name, ')
+ ..write('value: $value, ')
+ ..write('rowid: $rowid')
+ ..write(')'))
+ .toString();
+ }
+}
+
+class Accounts extends Table with TableInfo {
+ @override
+ final GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ Accounts(this.attachedDatabase, [this._alias]);
+ late final GeneratedColumn id = GeneratedColumn(
+ 'id',
+ aliasedName,
+ false,
+ hasAutoIncrement: true,
+ type: DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultConstraints: GeneratedColumn.constraintIsAlways(
+ 'PRIMARY KEY AUTOINCREMENT',
+ ),
+ );
+ late final GeneratedColumn realmUrl = GeneratedColumn(
+ 'realm_url',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn userId = GeneratedColumn(
+ 'user_id',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn email = GeneratedColumn(
+ 'email',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn apiKey = GeneratedColumn(
+ 'api_key',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn zulipVersion = GeneratedColumn(
+ 'zulip_version',
+ aliasedName,
+ false,
+ type: DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn zulipMergeBase = GeneratedColumn(
+ 'zulip_merge_base',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ late final GeneratedColumn zulipFeatureLevel = GeneratedColumn(
+ 'zulip_feature_level',
+ aliasedName,
+ false,
+ type: DriftSqlType.int,
+ requiredDuringInsert: true,
+ );
+ late final GeneratedColumn ackedPushToken = GeneratedColumn(
+ 'acked_push_token',
+ aliasedName,
+ true,
+ type: DriftSqlType.string,
+ requiredDuringInsert: false,
+ );
+ @override
+ List get $columns => [
+ id,
+ realmUrl,
+ userId,
+ email,
+ apiKey,
+ zulipVersion,
+ zulipMergeBase,
+ zulipFeatureLevel,
+ ackedPushToken,
+ ];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'accounts';
+ @override
+ Set get $primaryKey => {id};
+ @override
+ List> get uniqueKeys => [
+ {realmUrl, userId},
+ {realmUrl, email},
+ ];
+ @override
+ AccountsData map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return AccountsData(
+ id: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}id'],
+ )!,
+ realmUrl: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}realm_url'],
+ )!,
+ userId: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}user_id'],
+ )!,
+ email: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}email'],
+ )!,
+ apiKey: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}api_key'],
+ )!,
+ zulipVersion: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}zulip_version'],
+ )!,
+ zulipMergeBase: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}zulip_merge_base'],
+ ),
+ zulipFeatureLevel: attachedDatabase.typeMapping.read(
+ DriftSqlType.int,
+ data['${effectivePrefix}zulip_feature_level'],
+ )!,
+ ackedPushToken: attachedDatabase.typeMapping.read(
+ DriftSqlType.string,
+ data['${effectivePrefix}acked_push_token'],
+ ),
+ );
+ }
+
+ @override
+ Accounts createAlias(String alias) {
+ return Accounts(attachedDatabase, alias);
+ }
+}
+
+class AccountsData extends DataClass implements Insertable {
+ final int id;
+ final String realmUrl;
+ final int userId;
+ final String email;
+ final String apiKey;
+ final String zulipVersion;
+ final String? zulipMergeBase;
+ final int zulipFeatureLevel;
+ final String? ackedPushToken;
+ const AccountsData({
+ required this.id,
+ required this.realmUrl,
+ required this.userId,
+ required this.email,
+ required this.apiKey,
+ required this.zulipVersion,
+ this.zulipMergeBase,
+ required this.zulipFeatureLevel,
+ this.ackedPushToken,
+ });
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['id'] = Variable(id);
+ map['realm_url'] = Variable(realmUrl);
+ map['user_id'] = Variable(userId);
+ map['email'] = Variable(email);
+ map['api_key'] = Variable(apiKey);
+ map['zulip_version'] = Variable(zulipVersion);
+ if (!nullToAbsent || zulipMergeBase != null) {
+ map['zulip_merge_base'] = Variable(zulipMergeBase);
+ }
+ map['zulip_feature_level'] = Variable(zulipFeatureLevel);
+ if (!nullToAbsent || ackedPushToken != null) {
+ map['acked_push_token'] = Variable(ackedPushToken);
+ }
+ return map;
+ }
+
+ AccountsCompanion toCompanion(bool nullToAbsent) {
+ return AccountsCompanion(
+ id: Value(id),
+ realmUrl: Value(realmUrl),
+ userId: Value(userId),
+ email: Value(email),
+ apiKey: Value(apiKey),
+ zulipVersion: Value(zulipVersion),
+ zulipMergeBase: zulipMergeBase == null && nullToAbsent
+ ? const Value.absent()
+ : Value(zulipMergeBase),
+ zulipFeatureLevel: Value(zulipFeatureLevel),
+ ackedPushToken: ackedPushToken == null && nullToAbsent
+ ? const Value.absent()
+ : Value(ackedPushToken),
+ );
+ }
+
+ factory AccountsData.fromJson(
+ Map json, {
+ ValueSerializer? serializer,
+ }) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return AccountsData(
+ id: serializer.fromJson(json['id']),
+ realmUrl: serializer.fromJson(json['realmUrl']),
+ userId: serializer.fromJson(json['userId']),
+ email: serializer.fromJson(json['email']),
+ apiKey: serializer.fromJson(json['apiKey']),
+ zulipVersion: serializer.fromJson(json['zulipVersion']),
+ zulipMergeBase: serializer.fromJson(json['zulipMergeBase']),
+ zulipFeatureLevel: serializer.fromJson(json['zulipFeatureLevel']),
+ ackedPushToken: serializer.fromJson(json['ackedPushToken']),
+ );
+ }
+ @override
+ Map toJson({ValueSerializer? serializer}) {
+ serializer ??= driftRuntimeOptions.defaultSerializer;
+ return {
+ 'id': serializer.toJson(id),
+ 'realmUrl': serializer.toJson(realmUrl),
+ 'userId': serializer.toJson