Skip to content

Support language settings #1513

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions assets/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,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"
Expand Down
22 changes: 22 additions & 0 deletions docs/translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ is working correctly.

## Adding new UI strings

<div id="add-string" />

### Adding a string to the translation database

To add a new string in the UI, start by
Expand Down Expand Up @@ -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,
Expand Down
30 changes: 30 additions & 0 deletions lib/generated/l10n/zulip_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,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:
Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_ar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_de.dart
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_ja.dart
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_nb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_pl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_ru.dart
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,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 => 'Нет вопроса.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_sk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,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.';

Expand Down
15 changes: 15 additions & 0 deletions lib/generated/l10n/zulip_localizations_uk.dart
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,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 => 'Немає питання.';

Expand Down
61 changes: 60 additions & 1 deletion lib/model/database.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

import 'package:drift/drift.dart';
import 'package:drift/internal/versioned_schema.dart';
import 'package:drift/remote.dart';
Expand All @@ -24,6 +26,8 @@ class GlobalSettings extends Table {
Column<String> get browserPreference => textEnum<BrowserPreference>()
.nullable()();

Column<String> 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
Expand Down Expand Up @@ -119,7 +123,7 @@ class AppDatabase extends _$AppDatabase {
// information on using the build_runner.
// * Write a migration in `_migrationSteps` below.
// * Write tests.
static const int latestSchemaVersion = 6; // See note.
static const int latestSchemaVersion = 7; // See note.

@override
int get schemaVersion => latestSchemaVersion;
Expand Down Expand Up @@ -174,6 +178,9 @@ class AppDatabase extends _$AppDatabase {
from5To6: (m, schema) async {
await m.createTable(schema.boolGlobalSettings);
},
from6To7: (m, schema) async {
await m.addColumn(schema.globalSettings, schema.globalSettings.language);
}
);

Future<void> _createLatestSchema(Migrator m) async {
Expand Down Expand Up @@ -243,3 +250,55 @@ class AppDatabase extends _$AppDatabase {
}

class AccountAlreadyExistsException implements Exception {}

class LocaleConverter extends TypeConverter<Locale, String> {
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();
}
}
Loading