Browse Source

Merge branch 'main' into main

Gustavo Sanchez 3 years ago
parent
commit
e1f6e483e7
26 changed files with 733 additions and 210 deletions
  1. 12 0
      .github/workflows/commitlint.yml
  2. 3 0
      .gitignore
  3. 4 0
      .husky/commit-msg
  4. 32 0
      Makefile.toml
  5. 20 0
      commitlint.config.js
  6. 146 0
      frontend/app_flowy/assets/translations/ru-RU.json
  7. 15 9
      frontend/app_flowy/lib/startup/tasks/application_widget.dart
  8. 16 12
      frontend/app_flowy/lib/workspace/application/appearance.dart
  9. 6 3
      frontend/app_flowy/lib/workspace/presentation/home/navigation.dart
  10. 8 8
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart
  11. 7 2
      frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_page.dart
  12. 2 3
      frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart
  13. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart
  14. 5 1
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart
  15. 2 3
      frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_trash.dart
  16. 19 52
      frontend/app_flowy/packages/flowy_infra/lib/language.dart
  17. 0 10
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart
  18. 71 8
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user-data-model/user_setting.pb.dart
  19. 13 2
      frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user-data-model/user_setting.pbjson.dart
  20. 8 7
      frontend/rust-lib/flowy-user/src/handlers/user_handler.rs
  21. 1 1
      frontend/scripts/docker-buildfiles/Dockerfile
  22. 7 0
      package.json
  23. 42 41
      shared-lib/flowy-derive/src/derive_cache/derive_cache.rs
  24. 21 3
      shared-lib/flowy-user-data-model/src/entities/user_setting.rs
  25. 267 43
      shared-lib/flowy-user-data-model/src/protobuf/model/user_setting.rs
  26. 5 1
      shared-lib/flowy-user-data-model/src/protobuf/proto/user_setting.proto

+ 12 - 0
.github/workflows/commitlint.yml

@@ -0,0 +1,12 @@
+name: Lint Commit Messages
+on: [pull_request, push]
+
+jobs:
+  commitlint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0
+      - uses: wagoid/commitlint-github-action@v4
+

+ 3 - 0
.gitignore

@@ -15,3 +15,6 @@ Cargo.lock
 .idea/
 **/temp/**
 .ruby-version
+package-lock.json
+yarn.lock
+node_modules

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no -- commitlint --edit 

+ 32 - 0
Makefile.toml

@@ -0,0 +1,32 @@
+[tasks.install-commitlint]
+mac_alias = "install-commitlint-macos"
+windows_alias = "install-commitlint-windows"
+linux_alias = "install-commitlint-linux"
+
+[tasks.install-commitlint-macos]
+script = [
+    """
+    brew install npm
+	yarn install
+	yarn husky install
+    """,
+]
+script_runner = "@shell"
+
+[tasks.install-commitlint-windows]
+script = [
+    """
+    echo "WIP"
+    """,
+]
+script_runner = "@duckscript"
+
+[tasks.install-commitlint-linux]
+script = [
+    """
+    sudo apt install nodejs
+	yarn install
+	yarn husky install
+    """,
+]
+script_runner = "@duckscript"

+ 20 - 0
commitlint.config.js

@@ -0,0 +1,20 @@
+// module.exports = {extends: ['@commitlint/config-conventional']}
+module.exports = {
+    rules: {
+        'type-enum': [2, 'always', ['feat', 'refactor', 'style', 'fix', 'ci']],
+        'body-leading-blank': [1, 'always'],
+        'body-max-line-length': [2, 'always', 100],
+        'footer-leading-blank': [1, 'always'],
+        'footer-max-line-length': [2, 'always', 100],
+        'header-max-length': [2, 'always', 100],
+        'subject-case': [
+            2,
+            'never',
+            ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
+        ],
+        'subject-empty': [2, 'never'],
+        'type-empty': [2, 'never'],
+        'type-case': [2, 'always', 'lower-case'],
+        'body-case': [2, 'never', []]
+    },
+};

+ 146 - 0
frontend/app_flowy/assets/translations/ru-RU.json

@@ -0,0 +1,146 @@
+{
+    "appName": "AppFlowy",
+    "defaultUsername": "Я",
+    "welcomeText": "Добро пожаловать в @:appName",
+    "githubStarText": "Поставить звезду на GitHub",
+    "subscribeNewsletterText": "Подписаться на рассылку",
+    "letsGoButtonText": "Начнём",
+    "title": "Заголовок",
+    "signUp": {
+      "buttonText": "Зарегистрироваться",
+      "title": "Регистрация в @:appName",
+      "getStartedText": "Начать",
+      "emptyPasswordError": "Пароль не может быть пустым",
+      "repeatPasswordEmptyError": "Повтор пароля не может быть пустым",
+      "unmatchedPasswordError": "Пароли не совпадают",
+      "alreadyHaveAnAccount": "Уже есть аккаунт?",
+      "emailHint": "Электронная почта",
+      "passwordHint": "Пароль",
+      "repeatPasswordHint": "Повторите пароль"
+    },
+    "signIn": {
+      "loginTitle": "Войти в @:appName",
+      "loginButtonText": "Войти",
+      "buttonText": "Авторизация",
+      "forgotPassword": "Забыли пароль?",
+      "emailHint": "Электронная почта",
+      "passwordHint": "Пароль",
+      "dontHaveAnAccount": "Нет аккаунта?",
+      "repeatPasswordEmptyError": "Повтор пароля не может быть пустым",
+      "unmatchedPasswordError": "Пароли не совпадают"
+    },
+    "workspace": {
+      "create": "Создать рабочее пространство",
+      "hint": "рабочее пространство",
+      "notFoundError": "Нет такого рабочего пространства"
+    },
+    "shareAction": {
+      "buttonText": "Поделиться",
+      "workInProgress": "В разработке",
+      "markdown": "Markdown",
+      "copyLink": "Скопировать ссылку"
+    },
+    "disclosureAction": {
+      "rename": "Переименовать",
+      "delete": "Удалить",
+      "duplicate": "Дублировать"
+    },
+    "blankPageTitle": "Пустая страница",
+    "newPageText": "Новая страница",
+    "trash": {
+      "text": "Корзина",
+      "restoreAll": "Восстановить всё",
+      "deleteAll": "Очистить",
+      "pageHeader": {
+        "fileName": "Имя",
+        "lastModified": "Последнее изменение",
+        "created": "Создан"
+      }
+    },
+    "deletePagePrompt": {
+      "text": "Эта страница в Корзине",
+      "restore": "Восстановить страницу",
+      "deletePermanent": "Удалить навсегда"
+    },
+    "dialogCreatePageNameHint": "Имя",
+    "questionBubble": {
+      "whatsNew": "Что нового?",
+      "help": "Помощь",
+      "debug": {
+        "name": "Отладочная информация",
+        "success": "Скопировано в буфер обмена!",
+        "fail": "Не получилось скопировать"
+      }
+    },
+    "menuAppHeader": {
+      "addPageTooltip": "Быстро добавить новую страницу",
+      "defaultNewPageName": "Без заголовка",
+      "renameDialog": "Переименовать"
+    },
+    "toolbar": {
+      "undo": "Отменить",
+      "redo": "Повторить",
+      "bold": "Жирный",
+      "italic": "Курсив",
+      "underline": "Подчёркнутый",
+      "strike": "Зачёркнутый",
+      "numList": "Нумерованный список",
+      "bulletList": "Маркированный список",
+      "checkList": "Список To-Do",
+      "inlineCode": "Код",
+      "quote": "Цитата",
+      "header": "Заголовок",
+      "highlight": "Выделение"
+    },
+    "tooltip": {
+      "lightMode": "Переключиться в светлую тему",
+      "darkMode": "Переключиться в тёмную тему"
+    },
+    "contactsPage": {
+      "title": "Контакты",
+      "whatsHappening": "Что происходит на этой неделе?",
+      "addContact": "Новый контакт",
+      "editContact": "Редактировать"
+    },
+    "button": {
+      "OK": "OK",
+      "Cancel": "Отмена",
+      "signIn": "Войти",
+      "signOut": "Выйти",
+      "complete": "Завершить",
+      "save": "Сохранить"
+    },
+    "label": {
+      "welcome": "Добро пожаловать!",
+      "firstName": "Имя",
+      "middleName": "Отчество",
+      "lastName": "Фамилия",
+      "stepX": "Этап {X}"
+    },
+    "oAuth": {
+      "err": {
+        "failedTitle": "Ошибка подключения к аккаунту.",
+        "failedMsg": "Убедитесь, что вы завершили вход в своём браузере."
+      },
+      "google": {
+        "title": "Вход через Google",
+        "instruction1": "Чтобы импортировать ваши Google Контакты, вам нужно будет авторизовать приложение через браузер.",
+        "instruction2": "Скопируйте этот код в буфер обмена (нажав кнопку или выделив текст):",
+        "instruction3": "Пройдите по ссылке и введите этот код:",
+        "instruction4": "Нажмите на кнопку, когда завершите вход:"
+      }
+    },
+    "settings": {
+      "title": "Настройки",
+      "menu": {
+        "appearance": "Внешнией вид",
+        "language": "Язык",
+        "open": "Открыть настройки"
+      },
+      "appearance": {
+        "lightLabel": "Светлая тема",
+        "darkLabel": "Тёмная тема"
+      }
+    }
+  }
+  

+ 15 - 9
frontend/app_flowy/lib/startup/tasks/application_widget.dart

@@ -2,7 +2,6 @@ import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/user/infrastructure/repos/user_setting_repo.dart';
 import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/language.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
@@ -29,10 +28,17 @@ class AppWidgetTask extends LaunchTask {
       () {
         runApp(
           EasyLocalization(
-              supportedLocales: const [Locale('en'), Locale('zh', 'CN'), Locale('it', 'IT'), Locale('fr', 'CA'), Locale('es', 'VE')],
-              path: 'assets/translations',
-              fallbackLocale: const Locale('en'),
-              child: app),
+            supportedLocales: const [
+              Locale('en'),
+              Locale('zh', 'CN'),
+              Locale('it', 'IT'),
+              Locale('fr', 'CA'),
+              Locale('es', 'VE'),
+            ],
+            path: 'assets/translations',
+            fallbackLocale: const Locale('en'),
+            child: app,
+          ),
         );
       },
       blocObserver: ApplicationBlocObserver(),
@@ -63,14 +69,14 @@ class ApplicationWidget extends StatelessWidget {
           AppTheme theme = context.select<AppearanceSettingModel, AppTheme>(
             (value) => value.theme,
           );
-          AppLanguage language = context.select<AppearanceSettingModel, AppLanguage>(
-            (value) => value.language,
+          Locale locale = context.select<AppearanceSettingModel, Locale>(
+            (value) => value.locale,
           );
 
           return MultiProvider(
             providers: [
               Provider.value(value: theme),
-              Provider.value(value: language),
+              Provider.value(value: locale),
             ],
             builder: (context, _) {
               return MaterialApp(
@@ -79,7 +85,7 @@ class ApplicationWidget extends StatelessWidget {
                 theme: theme.themeData,
                 localizationsDelegates: context.localizationDelegates,
                 supportedLocales: context.supportedLocales,
-                locale: localeFromLanguageName(language),
+                locale: locale,
                 navigatorKey: AppGlobals.rootNavKey,
                 home: child,
               );

+ 16 - 12
frontend/app_flowy/lib/workspace/application/appearance.dart

@@ -1,7 +1,7 @@
 import 'package:app_flowy/user/infrastructure/repos/user_setting_repo.dart';
 import 'package:equatable/equatable.dart';
 import 'package:flowy_infra/theme.dart';
-import 'package:flowy_infra/language.dart';
+import 'package:flowy_log/flowy_log.dart';
 import 'package:flowy_sdk/protobuf/flowy-user-data-model/user_setting.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -10,15 +10,15 @@ import 'package:async/async.dart';
 class AppearanceSettingModel extends ChangeNotifier with EquatableMixin {
   AppearanceSettings setting;
   AppTheme _theme;
-  AppLanguage _language;
+  Locale _locale;
   CancelableOperation? _saveOperation;
 
   AppearanceSettingModel(this.setting)
       : _theme = AppTheme.fromName(name: setting.theme),
-        _language = languageFromString(setting.language);
+        _locale = Locale(setting.locale.languageCode, setting.locale.countryCode);
 
   AppTheme get theme => _theme;
-  AppLanguage get language => _language;
+  Locale get locale => _locale;
 
   Future<void> save() async {
     _saveOperation?.cancel;
@@ -45,13 +45,18 @@ class AppearanceSettingModel extends ChangeNotifier with EquatableMixin {
     }
   }
 
-  void setLanguage(BuildContext context, AppLanguage language) {
-    String languageString = stringFromLanguage(language);
+  void setLocale(BuildContext context, Locale newLocale) {
+    if (_locale != newLocale) {
+      if (!context.supportedLocales.contains(newLocale)) {
+        Log.error("Unsupported locale: $newLocale");
+        newLocale = const Locale('en');
+        Log.debug("Fallback to locale: $newLocale");
+      }
 
-    if (setting.language != languageString) {
-      context.setLocale(localeFromLanguageName(language));
-      _language = language;
-      setting.language = languageString;
+      context.setLocale(newLocale);
+      _locale = newLocale;
+      setting.locale.languageCode = _locale.languageCode;
+      setting.locale.countryCode = _locale.countryCode ?? "";
       notifyListeners();
       save();
     }
@@ -62,8 +67,7 @@ class AppearanceSettingModel extends ChangeNotifier with EquatableMixin {
       setting.resetAsDefault = false;
       save();
 
-      final language = languageFromLocale(context.deviceLocale);
-      setLanguage(context, language);
+      setLocale(context, context.deviceLocale);
     }
   }
 }

+ 6 - 3
frontend/app_flowy/lib/workspace/presentation/home/navigation.dart

@@ -1,6 +1,7 @@
 import 'package:app_flowy/workspace/domain/page_stack/page_stack.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/notifier.dart';
+import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
@@ -52,6 +53,8 @@ class FlowyNavigation extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
     return ChangeNotifierProxyProvider<HomeStackNotifier, NavigationNotifier>(
       create: (_) {
         final notifier = Provider.of<HomeStackNotifier>(context, listen: false);
@@ -65,7 +68,7 @@ class FlowyNavigation extends StatelessWidget {
         child: Row(children: [
           Selector<NavigationNotifier, PublishNotifier<bool>>(
               selector: (context, notifier) => notifier.collapasedNotifier,
-              builder: (ctx, collapsedNotifier, child) => _renderCollapse(ctx, collapsedNotifier)),
+              builder: (ctx, collapsedNotifier, child) => _renderCollapse(ctx, collapsedNotifier, theme)),
           Selector<NavigationNotifier, List<NavigationItem>>(
             selector: (context, notifier) => notifier.navigationItems,
             builder: (ctx, items, child) => Expanded(
@@ -80,7 +83,7 @@ class FlowyNavigation extends StatelessWidget {
     );
   }
 
-  Widget _renderCollapse(BuildContext context, PublishNotifier<bool> collapsedNotifier) {
+  Widget _renderCollapse(BuildContext context, PublishNotifier<bool> collapsedNotifier, AppTheme theme) {
     return ChangeNotifierProvider.value(
       value: collapsedNotifier,
       child: Consumer(
@@ -94,7 +97,7 @@ class FlowyNavigation extends StatelessWidget {
                   notifier.value = false;
                 },
                 iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
-                icon: svg("home/hide_menu"),
+                icon: svg("home/hide_menu", color: theme.iconColor),
               ),
             );
           } else {

+ 8 - 8
frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart

@@ -1,5 +1,5 @@
 import 'package:app_flowy/workspace/application/appearance.dart';
-import 'package:flutter/foundation.dart';
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flowy_infra/language.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -30,18 +30,18 @@ class LanguageSelectorDropdown extends StatefulWidget {
 class _LanguageSelectorDropdownState extends State<LanguageSelectorDropdown> {
   @override
   Widget build(BuildContext context) {
-    return DropdownButton<AppLanguage>(
-      value: context.read<AppearanceSettingModel>().language,
+    return DropdownButton<Locale>(
+      value: context.read<AppearanceSettingModel>().locale,
       onChanged: (val) {
         setState(() {
-          context.read<AppearanceSettingModel>().setLanguage(context, val!);
+          context.read<AppearanceSettingModel>().setLocale(context, val!);
         });
       },
       autofocus: true,
-      items: AppLanguage.values.map((language) {
-        return DropdownMenuItem<AppLanguage>(
-          value: language,
-          child: Text(describeEnum(language)),
+      items: EasyLocalization.of(context)!.supportedLocales.map((locale) {
+        return DropdownMenuItem<Locale>(
+          value: locale,
+          child: Text(languageFromLocale(locale)),
         );
       }).toList(),
     );

+ 7 - 2
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_page.dart

@@ -1,4 +1,5 @@
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/application/doc/doc_bloc.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -7,6 +8,7 @@ import 'package:flowy_infra_ui/widget/error_page.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:provider/provider.dart';
 import 'package:styled_widget/styled_widget.dart';
 import 'styles.dart';
 import 'widget/banner.dart';
@@ -119,8 +121,11 @@ class _DocPageState extends State<DocPage> {
   }
 
   Widget _renderToolbar(quill.QuillController controller) {
-    return EditorToolbar.basic(
-      controller: controller,
+    return ChangeNotifierProvider.value(
+      value: Provider.of<AppearanceSettingModel>(context, listen: true),
+      child: EditorToolbar.basic(
+        controller: controller,
+      ),
     );
   }
 

+ 2 - 3
frontend/app_flowy/lib/workspace/presentation/stack_page/doc/doc_stack_page.dart

@@ -7,7 +7,6 @@ import 'package:app_flowy/workspace/infrastructure/repos/view_repo.dart';
 import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
 import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra/language.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -175,8 +174,8 @@ class DocumentShareButton extends StatelessWidget {
           builder: (context, state) {
             return ChangeNotifierProvider.value(
               value: Provider.of<AppearanceSettingModel>(context, listen: true),
-              child: Selector<AppearanceSettingModel, AppLanguage>(
-                selector: (ctx, notifier) => notifier.language,
+              child: Selector<AppearanceSettingModel, Locale>(
+                selector: (ctx, notifier) => notifier.locale,
                 builder: (ctx, _, child) => ConstrainedBox(
                   constraints: const BoxConstraints.expand(
                     height: 30,

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/header/header.dart

@@ -26,7 +26,7 @@ class MenuAppHeader extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
+    final theme = context.read<AppTheme>();
     return SizedBox(
       height: MenuAppSizes.headerHeight,
       child: Row(

+ 5 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/app/menu_app.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/workspace/application/appearance.dart';
 import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
 import 'package:app_flowy/workspace/presentation/widgets/menu/widget/app/header/header.dart';
 import 'package:expandable/expandable.dart';
@@ -79,7 +80,10 @@ class _MenuAppState extends State<MenuApp> {
                 iconPadding: EdgeInsets.zero,
                 hasIcon: false,
               ),
-              header: MenuAppHeader(widget.app),
+              header: ChangeNotifierProvider.value(
+                value: Provider.of<AppearanceSettingModel>(context, listen: true),
+                child: MenuAppHeader(widget.app),
+              ),
               expanded: _renderViewSection(notifier),
               collapsed: const SizedBox(),
             ),

+ 2 - 3
frontend/app_flowy/lib/workspace/presentation/widgets/menu/widget/menu_trash.dart

@@ -5,7 +5,6 @@ import 'package:app_flowy/workspace/presentation/stack_page/trash/trash_page.dar
 import 'package:app_flowy/workspace/presentation/widgets/menu/menu.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra/language.dart';
 import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flutter/material.dart';
@@ -43,8 +42,8 @@ class MenuTrash extends StatelessWidget {
       const HSpace(6),
       ChangeNotifierProvider.value(
         value: Provider.of<AppearanceSettingModel>(context, listen: true),
-        child: Selector<AppearanceSettingModel, AppLanguage>(
-          selector: (ctx, notifier) => notifier.language,
+        child: Selector<AppearanceSettingModel, Locale>(
+          selector: (ctx, notifier) => notifier.locale,
           builder: (ctx, _, child) => FlowyText.medium(LocaleKeys.trash_text.tr(), fontSize: 12),
         ),
       ),

+ 19 - 52
frontend/app_flowy/packages/flowy_infra/lib/language.dart

@@ -1,60 +1,27 @@
 import 'package:flutter/material.dart';
 
-enum AppLanguage {
-  english,
-  chinese,
-  italian,
-  french,
-}
-
-String stringFromLanguage(AppLanguage language) {
-  switch (language) {
-    case AppLanguage.english:
-      return "en";
-    case AppLanguage.chinese:
-      return "ch";
-    case AppLanguage.italian:
-      return "it";
-    case AppLanguage.french:
-      return "fr";
-  }
-}
-
-AppLanguage languageFromString(String name) {
-  AppLanguage language = AppLanguage.english;
-  if (name == "ch") {
-    language = AppLanguage.chinese;
-  } else if (name == "it") {
-    language = AppLanguage.italian;
-  } else if (name == "fr") {
-    language = AppLanguage.french;
-  }
-
-  return language;
-}
-
-Locale localeFromLanguageName(AppLanguage language) {
-  switch (language) {
-    case AppLanguage.english:
-      return const Locale('en');
-    case AppLanguage.chinese:
-      return const Locale('zh', 'CN');
-    case AppLanguage.italian:
-      return const Locale('it', 'IT');
-    case AppLanguage.french:
-      return const Locale('fr', 'CA');
-  }
-}
-
-AppLanguage languageFromLocale(Locale locale) {
+String languageFromLocale(Locale locale) {
   switch (locale.languageCode) {
+    // Most often used languages
+    case "en":
+      return "English";
     case "zh":
-      return AppLanguage.chinese;
-    case "it":
-      return AppLanguage.italian;
+      return "简体中文";
+
+    // Then in alphabetical order
+    case "de":
+      return "Deutsch";
+    case "es":
+      return "Español";
     case "fr":
-      return AppLanguage.french;
+      return "Français";
+    case "it":
+      return "Italiano";
+    case "ru":
+      return "русский";
+
+    // If not found then the language code will be displayed
     default:
-      return AppLanguage.english;
+      return locale.languageCode;
   }
 }

+ 0 - 10
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/icon_button.dart

@@ -30,16 +30,6 @@ class FlowyIconButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     Widget child = icon;
-
-    // if (onPressed == null) {
-    //   child = ColorFiltered(
-    //     colorFilter: ColorFilter.mode(
-    //       Colors.grey,
-    //       BlendMode.saturation,
-    //     ),
-    //     child: child,
-    //   );
-    // }
     final size = Size(width, height ?? width);
 
     assert(size.width > iconPadding.horizontal);

+ 71 - 8
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user-data-model/user_setting.pb.dart

@@ -75,7 +75,7 @@ class UserPreferences extends $pb.GeneratedMessage {
 class AppearanceSettings extends $pb.GeneratedMessage {
   static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'AppearanceSettings', createEmptyInstance: create)
     ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'theme')
-    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'language')
+    ..aOM<LocaleSettings>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'locale', subBuilder: LocaleSettings.create)
     ..aOB(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'resetAsDefault')
     ..hasRequiredFields = false
   ;
@@ -83,15 +83,15 @@ class AppearanceSettings extends $pb.GeneratedMessage {
   AppearanceSettings._() : super();
   factory AppearanceSettings({
     $core.String? theme,
-    $core.String? language,
+    LocaleSettings? locale,
     $core.bool? resetAsDefault,
   }) {
     final _result = create();
     if (theme != null) {
       _result.theme = theme;
     }
-    if (language != null) {
-      _result.language = language;
+    if (locale != null) {
+      _result.locale = locale;
     }
     if (resetAsDefault != null) {
       _result.resetAsDefault = resetAsDefault;
@@ -129,13 +129,15 @@ class AppearanceSettings extends $pb.GeneratedMessage {
   void clearTheme() => clearField(1);
 
   @$pb.TagNumber(2)
-  $core.String get language => $_getSZ(1);
+  LocaleSettings get locale => $_getN(1);
   @$pb.TagNumber(2)
-  set language($core.String v) { $_setString(1, v); }
+  set locale(LocaleSettings v) { setField(2, v); }
   @$pb.TagNumber(2)
-  $core.bool hasLanguage() => $_has(1);
+  $core.bool hasLocale() => $_has(1);
   @$pb.TagNumber(2)
-  void clearLanguage() => clearField(2);
+  void clearLocale() => clearField(2);
+  @$pb.TagNumber(2)
+  LocaleSettings ensureLocale() => $_ensure(1);
 
   @$pb.TagNumber(3)
   $core.bool get resetAsDefault => $_getBF(2);
@@ -147,3 +149,64 @@ class AppearanceSettings extends $pb.GeneratedMessage {
   void clearResetAsDefault() => clearField(3);
 }
 
+class LocaleSettings extends $pb.GeneratedMessage {
+  static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'LocaleSettings', createEmptyInstance: create)
+    ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'languageCode')
+    ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'countryCode')
+    ..hasRequiredFields = false
+  ;
+
+  LocaleSettings._() : super();
+  factory LocaleSettings({
+    $core.String? languageCode,
+    $core.String? countryCode,
+  }) {
+    final _result = create();
+    if (languageCode != null) {
+      _result.languageCode = languageCode;
+    }
+    if (countryCode != null) {
+      _result.countryCode = countryCode;
+    }
+    return _result;
+  }
+  factory LocaleSettings.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
+  factory LocaleSettings.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
+  'Will be removed in next major version')
+  LocaleSettings clone() => LocaleSettings()..mergeFromMessage(this);
+  @$core.Deprecated(
+  'Using this can add significant overhead to your binary. '
+  'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
+  'Will be removed in next major version')
+  LocaleSettings copyWith(void Function(LocaleSettings) updates) => super.copyWith((message) => updates(message as LocaleSettings)) as LocaleSettings; // ignore: deprecated_member_use
+  $pb.BuilderInfo get info_ => _i;
+  @$core.pragma('dart2js:noInline')
+  static LocaleSettings create() => LocaleSettings._();
+  LocaleSettings createEmptyInstance() => create();
+  static $pb.PbList<LocaleSettings> createRepeated() => $pb.PbList<LocaleSettings>();
+  @$core.pragma('dart2js:noInline')
+  static LocaleSettings getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<LocaleSettings>(create);
+  static LocaleSettings? _defaultInstance;
+
+  @$pb.TagNumber(1)
+  $core.String get languageCode => $_getSZ(0);
+  @$pb.TagNumber(1)
+  set languageCode($core.String v) { $_setString(0, v); }
+  @$pb.TagNumber(1)
+  $core.bool hasLanguageCode() => $_has(0);
+  @$pb.TagNumber(1)
+  void clearLanguageCode() => clearField(1);
+
+  @$pb.TagNumber(2)
+  $core.String get countryCode => $_getSZ(1);
+  @$pb.TagNumber(2)
+  set countryCode($core.String v) { $_setString(1, v); }
+  @$pb.TagNumber(2)
+  $core.bool hasCountryCode() => $_has(1);
+  @$pb.TagNumber(2)
+  void clearCountryCode() => clearField(2);
+}
+

+ 13 - 2
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-user-data-model/user_setting.pbjson.dart

@@ -24,10 +24,21 @@ const AppearanceSettings$json = const {
   '1': 'AppearanceSettings',
   '2': const [
     const {'1': 'theme', '3': 1, '4': 1, '5': 9, '10': 'theme'},
-    const {'1': 'language', '3': 2, '4': 1, '5': 9, '10': 'language'},
+    const {'1': 'locale', '3': 2, '4': 1, '5': 11, '6': '.LocaleSettings', '10': 'locale'},
     const {'1': 'reset_as_default', '3': 3, '4': 1, '5': 8, '10': 'resetAsDefault'},
   ],
 };
 
 /// Descriptor for `AppearanceSettings`. Decode as a `google.protobuf.DescriptorProto`.
-final $typed_data.Uint8List appearanceSettingsDescriptor = $convert.base64Decode('ChJBcHBlYXJhbmNlU2V0dGluZ3MSFAoFdGhlbWUYASABKAlSBXRoZW1lEhoKCGxhbmd1YWdlGAIgASgJUghsYW5ndWFnZRIoChByZXNldF9hc19kZWZhdWx0GAMgASgIUg5yZXNldEFzRGVmYXVsdA==');
+final $typed_data.Uint8List appearanceSettingsDescriptor = $convert.base64Decode('ChJBcHBlYXJhbmNlU2V0dGluZ3MSFAoFdGhlbWUYASABKAlSBXRoZW1lEicKBmxvY2FsZRgCIAEoCzIPLkxvY2FsZVNldHRpbmdzUgZsb2NhbGUSKAoQcmVzZXRfYXNfZGVmYXVsdBgDIAEoCFIOcmVzZXRBc0RlZmF1bHQ=');
+@$core.Deprecated('Use localeSettingsDescriptor instead')
+const LocaleSettings$json = const {
+  '1': 'LocaleSettings',
+  '2': const [
+    const {'1': 'language_code', '3': 1, '4': 1, '5': 9, '10': 'languageCode'},
+    const {'1': 'country_code', '3': 2, '4': 1, '5': 9, '10': 'countryCode'},
+  ],
+};
+
+/// Descriptor for `LocaleSettings`. Decode as a `google.protobuf.DescriptorProto`.
+final $typed_data.Uint8List localeSettingsDescriptor = $convert.base64Decode('Cg5Mb2NhbGVTZXR0aW5ncxIjCg1sYW5ndWFnZV9jb2RlGAEgASgJUgxsYW5ndWFnZUNvZGUSIQoMY291bnRyeV9jb2RlGAIgASgJUgtjb3VudHJ5Q29kZQ==');

+ 8 - 7
frontend/rust-lib/flowy-user/src/handlers/user_handler.rs

@@ -1,8 +1,7 @@
 use crate::{errors::FlowyError, services::UserSession};
 use flowy_database::kv::KV;
 use flowy_user_data_model::entities::{
-    AppearanceSettings, UpdateUserParams, UpdateUserRequest, UserProfile, APPEARANCE_DEFAULT_LANGUAGE,
-    APPEARANCE_DEFAULT_THEME,
+    AppearanceSettings, UpdateUserParams, UpdateUserRequest, UserProfile, APPEARANCE_DEFAULT_THEME,
 };
 use lib_dispatch::prelude::*;
 use std::{convert::TryInto, sync::Arc};
@@ -50,10 +49,6 @@ pub async fn set_appearance_setting(data: Data<AppearanceSettings>) -> Result<()
         setting.theme = APPEARANCE_DEFAULT_THEME.to_string();
     }
 
-    if setting.language.is_empty() {
-        setting.theme = APPEARANCE_DEFAULT_LANGUAGE.to_string();
-    }
-
     let s = serde_json::to_string(&setting)?;
     KV::set_str(APPEARANCE_SETTING_CACHE_KEY, s);
     Ok(())
@@ -64,7 +59,13 @@ pub async fn get_appearance_setting() -> DataResult<AppearanceSettings, FlowyErr
     match KV::get_str(APPEARANCE_SETTING_CACHE_KEY) {
         None => data_result(AppearanceSettings::default()),
         Some(s) => {
-            let setting: AppearanceSettings = serde_json::from_str(&s)?;
+            let setting = match serde_json::from_str(&s) {
+                Ok(setting) => setting,
+                Err(e) => {
+                    tracing::error!("Deserialize AppearanceSettings failed: {:?}, fallback to default", e);
+                    AppearanceSettings::default()
+                }
+            };
             data_result(setting)
         }
     }

+ 1 - 1
frontend/scripts/docker-buildfiles/Dockerfile

@@ -35,4 +35,4 @@ cargo install --force cargo-make && \
 cargo install --force duckscript_cli && \
 cargo make flowy_dev && \
 cargo make -p production-linux-x86 appflowy-linux
-CMD ["appflowy/frontend/app_flowy/product/0.0.2/linux/Release/AppFlowy/app_flowy"]
+CMD ["appflowy/frontend/app_flowy/product/0.0.3/linux/Release/AppFlowy/app_flowy"]

+ 7 - 0
package.json

@@ -0,0 +1,7 @@
+{
+  "devDependencies": {
+    "@commitlint/cli": "16.1.0",
+    "@commitlint/config-conventional": "16.0.0",
+    "husky": "7.0.4"
+  }
+}

+ 42 - 41
shared-lib/flowy-derive/src/derive_cache/derive_cache.rs

@@ -18,48 +18,22 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         "String" => TypeCategory::Str,
         "FFIRequest"
         | "FFIResponse"
-        | "FlowyError"
         | "SubscribeObject"
+        | "FlowyError"
         | "NetworkState"
+        | "UserToken"
+        | "UserProfile"
+        | "UpdateUserRequest"
+        | "UpdateUserParams"
         | "SignInRequest"
         | "SignInParams"
         | "SignInResponse"
         | "SignUpRequest"
         | "SignUpParams"
         | "SignUpResponse"
-        | "UserToken"
-        | "UserProfile"
-        | "UpdateUserRequest"
-        | "UpdateUserParams"
         | "UserPreferences"
         | "AppearanceSettings"
-        | "ClientRevisionWSData"
-        | "ServerRevisionWSData"
-        | "NewDocumentUser"
-        | "FolderInfo"
-        | "Revision"
-        | "RepeatedRevision"
-        | "RevId"
-        | "RevisionRange"
-        | "CreateDocParams"
-        | "DocumentInfo"
-        | "ResetDocumentParams"
-        | "DocumentDelta"
-        | "NewDocUser"
-        | "DocumentId"
-        | "WSError"
-        | "WebSocketRawMessage"
-        | "Workspace"
-        | "RepeatedWorkspace"
-        | "CreateWorkspaceRequest"
-        | "CreateWorkspaceParams"
-        | "QueryWorkspaceRequest"
-        | "WorkspaceId"
-        | "CurrentWorkspaceSetting"
-        | "UpdateWorkspaceRequest"
-        | "UpdateWorkspaceParams"
-        | "ExportRequest"
-        | "ExportData"
+        | "LocaleSettings"
         | "App"
         | "RepeatedApp"
         | "CreateAppRequest"
@@ -69,10 +43,8 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         | "AppId"
         | "UpdateAppRequest"
         | "UpdateAppParams"
-        | "Trash"
-        | "RepeatedTrash"
-        | "RepeatedTrashId"
-        | "TrashId"
+        | "ExportRequest"
+        | "ExportData"
         | "View"
         | "RepeatedView"
         | "CreateViewRequest"
@@ -82,23 +54,52 @@ pub fn category_from_str(type_str: &str) -> TypeCategory {
         | "RepeatedViewId"
         | "UpdateViewRequest"
         | "UpdateViewParams"
+        | "Trash"
+        | "RepeatedTrash"
+        | "RepeatedTrashId"
+        | "TrashId"
+        | "Workspace"
+        | "RepeatedWorkspace"
+        | "CreateWorkspaceRequest"
+        | "CreateWorkspaceParams"
+        | "QueryWorkspaceRequest"
+        | "WorkspaceId"
+        | "CurrentWorkspaceSetting"
+        | "UpdateWorkspaceRequest"
+        | "UpdateWorkspaceParams"
+        | "ClientRevisionWSData"
+        | "ServerRevisionWSData"
+        | "NewDocumentUser"
+        | "CreateDocParams"
+        | "DocumentInfo"
+        | "ResetDocumentParams"
+        | "DocumentDelta"
+        | "NewDocUser"
+        | "DocumentId"
+        | "Revision"
+        | "RepeatedRevision"
+        | "RevId"
+        | "RevisionRange"
+        | "FolderInfo"
+        | "WSError"
+        | "WebSocketRawMessage"
         => TypeCategory::Protobuf,
         "FFIStatusCode"
         | "FolderEvent"
         | "FolderNotification"
-        | "NetworkEvent"
-        | "NetworkType"
         | "UserEvent"
         | "UserNotification"
+        | "NetworkEvent"
+        | "NetworkType"
+        | "ExportType"
+        | "ViewType"
+        | "TrashType"
         | "ClientRevisionWSDataType"
         | "ServerRevisionWSDataType"
         | "RevisionState"
         | "RevType"
         | "ErrorCode"
         | "WSChannel"
-        | "ExportType"
-        | "TrashType"
-        | "ViewType"
         => TypeCategory::Enum,
 
         "Option" => TypeCategory::Opt,

+ 21 - 3
shared-lib/flowy-user-data-model/src/entities/user_setting.rs

@@ -16,26 +16,44 @@ pub struct AppearanceSettings {
     pub theme: String,
 
     #[pb(index = 2)]
-    pub language: String,
+    #[serde(default)]
+    pub locale: LocaleSettings,
 
     #[pb(index = 3)]
     #[serde(default = "reset_default_value")]
     pub reset_as_default: bool,
 }
 
+#[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)]
+pub struct LocaleSettings {
+    #[pb(index = 1)]
+    pub language_code: String,
+
+    #[pb(index = 2)]
+    pub country_code: String,
+}
+
+impl std::default::Default for LocaleSettings {
+    fn default() -> Self {
+        Self {
+            language_code: "en".to_owned(),
+            country_code: "".to_owned(),
+        }
+    }
+}
+
 fn reset_default_value() -> bool {
     APPEARANCE_RESET_AS_DEFAULT
 }
 
 pub const APPEARANCE_DEFAULT_THEME: &str = "light";
-pub const APPEARANCE_DEFAULT_LANGUAGE: &str = "en";
 pub const APPEARANCE_RESET_AS_DEFAULT: bool = true;
 
 impl std::default::Default for AppearanceSettings {
     fn default() -> Self {
         AppearanceSettings {
             theme: APPEARANCE_DEFAULT_THEME.to_owned(),
-            language: APPEARANCE_DEFAULT_LANGUAGE.to_owned(),
+            locale: LocaleSettings::default(),
             reset_as_default: APPEARANCE_RESET_AS_DEFAULT,
         }
     }

+ 267 - 43
shared-lib/flowy-user-data-model/src/protobuf/model/user_setting.rs

@@ -243,7 +243,7 @@ impl ::protobuf::reflect::ProtobufValue for UserPreferences {
 pub struct AppearanceSettings {
     // message fields
     pub theme: ::std::string::String,
-    pub language: ::std::string::String,
+    pub locale: ::protobuf::SingularPtrField<LocaleSettings>,
     pub reset_as_default: bool,
     // special fields
     pub unknown_fields: ::protobuf::UnknownFields,
@@ -287,30 +287,37 @@ impl AppearanceSettings {
         ::std::mem::replace(&mut self.theme, ::std::string::String::new())
     }
 
-    // string language = 2;
+    // .LocaleSettings locale = 2;
 
 
-    pub fn get_language(&self) -> &str {
-        &self.language
+    pub fn get_locale(&self) -> &LocaleSettings {
+        self.locale.as_ref().unwrap_or_else(|| <LocaleSettings as ::protobuf::Message>::default_instance())
     }
-    pub fn clear_language(&mut self) {
-        self.language.clear();
+    pub fn clear_locale(&mut self) {
+        self.locale.clear();
+    }
+
+    pub fn has_locale(&self) -> bool {
+        self.locale.is_some()
     }
 
     // Param is passed by value, moved
-    pub fn set_language(&mut self, v: ::std::string::String) {
-        self.language = v;
+    pub fn set_locale(&mut self, v: LocaleSettings) {
+        self.locale = ::protobuf::SingularPtrField::some(v);
     }
 
     // Mutable pointer to the field.
     // If field is not initialized, it is initialized with default value first.
-    pub fn mut_language(&mut self) -> &mut ::std::string::String {
-        &mut self.language
+    pub fn mut_locale(&mut self) -> &mut LocaleSettings {
+        if self.locale.is_none() {
+            self.locale.set_default();
+        }
+        self.locale.as_mut().unwrap()
     }
 
     // Take field
-    pub fn take_language(&mut self) -> ::std::string::String {
-        ::std::mem::replace(&mut self.language, ::std::string::String::new())
+    pub fn take_locale(&mut self) -> LocaleSettings {
+        self.locale.take().unwrap_or_else(|| LocaleSettings::new())
     }
 
     // bool reset_as_default = 3;
@@ -331,6 +338,11 @@ impl AppearanceSettings {
 
 impl ::protobuf::Message for AppearanceSettings {
     fn is_initialized(&self) -> bool {
+        for v in &self.locale {
+            if !v.is_initialized() {
+                return false;
+            }
+        };
         true
     }
 
@@ -342,7 +354,7 @@ impl ::protobuf::Message for AppearanceSettings {
                     ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.theme)?;
                 },
                 2 => {
-                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.language)?;
+                    ::protobuf::rt::read_singular_message_into(wire_type, is, &mut self.locale)?;
                 },
                 3 => {
                     if wire_type != ::protobuf::wire_format::WireTypeVarint {
@@ -366,8 +378,9 @@ impl ::protobuf::Message for AppearanceSettings {
         if !self.theme.is_empty() {
             my_size += ::protobuf::rt::string_size(1, &self.theme);
         }
-        if !self.language.is_empty() {
-            my_size += ::protobuf::rt::string_size(2, &self.language);
+        if let Some(ref v) = self.locale.as_ref() {
+            let len = v.compute_size();
+            my_size += 1 + ::protobuf::rt::compute_raw_varint32_size(len) + len;
         }
         if self.reset_as_default != false {
             my_size += 2;
@@ -381,8 +394,10 @@ impl ::protobuf::Message for AppearanceSettings {
         if !self.theme.is_empty() {
             os.write_string(1, &self.theme)?;
         }
-        if !self.language.is_empty() {
-            os.write_string(2, &self.language)?;
+        if let Some(ref v) = self.locale.as_ref() {
+            os.write_tag(2, ::protobuf::wire_format::WireTypeLengthDelimited)?;
+            os.write_raw_varint32(v.get_cached_size())?;
+            v.write_to_with_cached_sizes(os)?;
         }
         if self.reset_as_default != false {
             os.write_bool(3, self.reset_as_default)?;
@@ -430,10 +445,10 @@ impl ::protobuf::Message for AppearanceSettings {
                 |m: &AppearanceSettings| { &m.theme },
                 |m: &mut AppearanceSettings| { &mut m.theme },
             ));
-            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
-                "language",
-                |m: &AppearanceSettings| { &m.language },
-                |m: &mut AppearanceSettings| { &mut m.language },
+            fields.push(::protobuf::reflect::accessor::make_singular_ptr_field_accessor::<_, ::protobuf::types::ProtobufTypeMessage<LocaleSettings>>(
+                "locale",
+                |m: &AppearanceSettings| { &m.locale },
+                |m: &mut AppearanceSettings| { &mut m.locale },
             ));
             fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeBool>(
                 "reset_as_default",
@@ -457,7 +472,7 @@ impl ::protobuf::Message for AppearanceSettings {
 impl ::protobuf::Clear for AppearanceSettings {
     fn clear(&mut self) {
         self.theme.clear();
-        self.language.clear();
+        self.locale.clear();
         self.reset_as_default = false;
         self.unknown_fields.clear();
     }
@@ -475,30 +490,239 @@ impl ::protobuf::reflect::ProtobufValue for AppearanceSettings {
     }
 }
 
+#[derive(PartialEq,Clone,Default)]
+pub struct LocaleSettings {
+    // message fields
+    pub language_code: ::std::string::String,
+    pub country_code: ::std::string::String,
+    // special fields
+    pub unknown_fields: ::protobuf::UnknownFields,
+    pub cached_size: ::protobuf::CachedSize,
+}
+
+impl<'a> ::std::default::Default for &'a LocaleSettings {
+    fn default() -> &'a LocaleSettings {
+        <LocaleSettings as ::protobuf::Message>::default_instance()
+    }
+}
+
+impl LocaleSettings {
+    pub fn new() -> LocaleSettings {
+        ::std::default::Default::default()
+    }
+
+    // string language_code = 1;
+
+
+    pub fn get_language_code(&self) -> &str {
+        &self.language_code
+    }
+    pub fn clear_language_code(&mut self) {
+        self.language_code.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_language_code(&mut self, v: ::std::string::String) {
+        self.language_code = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_language_code(&mut self) -> &mut ::std::string::String {
+        &mut self.language_code
+    }
+
+    // Take field
+    pub fn take_language_code(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.language_code, ::std::string::String::new())
+    }
+
+    // string country_code = 2;
+
+
+    pub fn get_country_code(&self) -> &str {
+        &self.country_code
+    }
+    pub fn clear_country_code(&mut self) {
+        self.country_code.clear();
+    }
+
+    // Param is passed by value, moved
+    pub fn set_country_code(&mut self, v: ::std::string::String) {
+        self.country_code = v;
+    }
+
+    // Mutable pointer to the field.
+    // If field is not initialized, it is initialized with default value first.
+    pub fn mut_country_code(&mut self) -> &mut ::std::string::String {
+        &mut self.country_code
+    }
+
+    // Take field
+    pub fn take_country_code(&mut self) -> ::std::string::String {
+        ::std::mem::replace(&mut self.country_code, ::std::string::String::new())
+    }
+}
+
+impl ::protobuf::Message for LocaleSettings {
+    fn is_initialized(&self) -> bool {
+        true
+    }
+
+    fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        while !is.eof()? {
+            let (field_number, wire_type) = is.read_tag_unpack()?;
+            match field_number {
+                1 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.language_code)?;
+                },
+                2 => {
+                    ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.country_code)?;
+                },
+                _ => {
+                    ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?;
+                },
+            };
+        }
+        ::std::result::Result::Ok(())
+    }
+
+    // Compute sizes of nested messages
+    #[allow(unused_variables)]
+    fn compute_size(&self) -> u32 {
+        let mut my_size = 0;
+        if !self.language_code.is_empty() {
+            my_size += ::protobuf::rt::string_size(1, &self.language_code);
+        }
+        if !self.country_code.is_empty() {
+            my_size += ::protobuf::rt::string_size(2, &self.country_code);
+        }
+        my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields());
+        self.cached_size.set(my_size);
+        my_size
+    }
+
+    fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> {
+        if !self.language_code.is_empty() {
+            os.write_string(1, &self.language_code)?;
+        }
+        if !self.country_code.is_empty() {
+            os.write_string(2, &self.country_code)?;
+        }
+        os.write_unknown_fields(self.get_unknown_fields())?;
+        ::std::result::Result::Ok(())
+    }
+
+    fn get_cached_size(&self) -> u32 {
+        self.cached_size.get()
+    }
+
+    fn get_unknown_fields(&self) -> &::protobuf::UnknownFields {
+        &self.unknown_fields
+    }
+
+    fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields {
+        &mut self.unknown_fields
+    }
+
+    fn as_any(&self) -> &dyn (::std::any::Any) {
+        self as &dyn (::std::any::Any)
+    }
+    fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) {
+        self as &mut dyn (::std::any::Any)
+    }
+    fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn (::std::any::Any)> {
+        self
+    }
+
+    fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor {
+        Self::descriptor_static()
+    }
+
+    fn new() -> LocaleSettings {
+        LocaleSettings::new()
+    }
+
+    fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor {
+        static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT;
+        descriptor.get(|| {
+            let mut fields = ::std::vec::Vec::new();
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "language_code",
+                |m: &LocaleSettings| { &m.language_code },
+                |m: &mut LocaleSettings| { &mut m.language_code },
+            ));
+            fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>(
+                "country_code",
+                |m: &LocaleSettings| { &m.country_code },
+                |m: &mut LocaleSettings| { &mut m.country_code },
+            ));
+            ::protobuf::reflect::MessageDescriptor::new_pb_name::<LocaleSettings>(
+                "LocaleSettings",
+                fields,
+                file_descriptor_proto()
+            )
+        })
+    }
+
+    fn default_instance() -> &'static LocaleSettings {
+        static instance: ::protobuf::rt::LazyV2<LocaleSettings> = ::protobuf::rt::LazyV2::INIT;
+        instance.get(LocaleSettings::new)
+    }
+}
+
+impl ::protobuf::Clear for LocaleSettings {
+    fn clear(&mut self) {
+        self.language_code.clear();
+        self.country_code.clear();
+        self.unknown_fields.clear();
+    }
+}
+
+impl ::std::fmt::Debug for LocaleSettings {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
+        ::protobuf::text_format::fmt(self, f)
+    }
+}
+
+impl ::protobuf::reflect::ProtobufValue for LocaleSettings {
+    fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef {
+        ::protobuf::reflect::ReflectValueRef::Message(self)
+    }
+}
+
 static file_descriptor_proto_data: &'static [u8] = b"\
     \n\x12user_setting.proto\"n\n\x0fUserPreferences\x12\x17\n\x07user_id\
     \x18\x01\x20\x01(\tR\x06userId\x12B\n\x12appearance_setting\x18\x02\x20\
-    \x01(\x0b2\x13.AppearanceSettingsR\x11appearanceSetting\"p\n\x12Appearan\
-    ceSettings\x12\x14\n\x05theme\x18\x01\x20\x01(\tR\x05theme\x12\x1a\n\x08\
-    language\x18\x02\x20\x01(\tR\x08language\x12(\n\x10reset_as_default\x18\
-    \x03\x20\x01(\x08R\x0eresetAsDefaultJ\xd5\x02\n\x06\x12\x04\0\0\n\x01\n\
-    \x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\x05\x01\n\n\
-    \n\x03\x04\0\x01\x12\x03\x02\x08\x17\n\x0b\n\x04\x04\0\x02\0\x12\x03\x03\
-    \x04\x17\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\n\x05\x04\0\
-    \x02\0\x01\x12\x03\x03\x0b\x12\n\x0c\n\x05\x04\0\x02\0\x03\x12\x03\x03\
-    \x15\x16\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04.\n\x0c\n\x05\x04\0\
-    \x02\x01\x06\x12\x03\x04\x04\x16\n\x0c\n\x05\x04\0\x02\x01\x01\x12\x03\
-    \x04\x17)\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04,-\n\n\n\x02\x04\x01\
-    \x12\x04\x06\0\n\x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x1a\n\x0b\n\
-    \x04\x04\x01\x02\0\x12\x03\x07\x04\x15\n\x0c\n\x05\x04\x01\x02\0\x05\x12\
-    \x03\x07\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\x10\n\x0c\n\
-    \x05\x04\x01\x02\0\x03\x12\x03\x07\x13\x14\n\x0b\n\x04\x04\x01\x02\x01\
-    \x12\x03\x08\x04\x18\n\x0c\n\x05\x04\x01\x02\x01\x05\x12\x03\x08\x04\n\n\
-    \x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\x08\x0b\x13\n\x0c\n\x05\x04\x01\
-    \x02\x01\x03\x12\x03\x08\x16\x17\n\x0b\n\x04\x04\x01\x02\x02\x12\x03\t\
-    \x04\x1e\n\x0c\n\x05\x04\x01\x02\x02\x05\x12\x03\t\x04\x08\n\x0c\n\x05\
-    \x04\x01\x02\x02\x01\x12\x03\t\t\x19\n\x0c\n\x05\x04\x01\x02\x02\x03\x12\
-    \x03\t\x1c\x1db\x06proto3\
+    \x01(\x0b2\x13.AppearanceSettingsR\x11appearanceSetting\"}\n\x12Appearan\
+    ceSettings\x12\x14\n\x05theme\x18\x01\x20\x01(\tR\x05theme\x12'\n\x06loc\
+    ale\x18\x02\x20\x01(\x0b2\x0f.LocaleSettingsR\x06locale\x12(\n\x10reset_\
+    as_default\x18\x03\x20\x01(\x08R\x0eresetAsDefault\"X\n\x0eLocaleSetting\
+    s\x12#\n\rlanguage_code\x18\x01\x20\x01(\tR\x0clanguageCode\x12!\n\x0cco\
+    untry_code\x18\x02\x20\x01(\tR\x0bcountryCodeJ\xdb\x03\n\x06\x12\x04\0\0\
+    \x0e\x01\n\x08\n\x01\x0c\x12\x03\0\0\x12\n\n\n\x02\x04\0\x12\x04\x02\0\
+    \x05\x01\n\n\n\x03\x04\0\x01\x12\x03\x02\x08\x17\n\x0b\n\x04\x04\0\x02\0\
+    \x12\x03\x03\x04\x17\n\x0c\n\x05\x04\0\x02\0\x05\x12\x03\x03\x04\n\n\x0c\
+    \n\x05\x04\0\x02\0\x01\x12\x03\x03\x0b\x12\n\x0c\n\x05\x04\0\x02\0\x03\
+    \x12\x03\x03\x15\x16\n\x0b\n\x04\x04\0\x02\x01\x12\x03\x04\x04.\n\x0c\n\
+    \x05\x04\0\x02\x01\x06\x12\x03\x04\x04\x16\n\x0c\n\x05\x04\0\x02\x01\x01\
+    \x12\x03\x04\x17)\n\x0c\n\x05\x04\0\x02\x01\x03\x12\x03\x04,-\n\n\n\x02\
+    \x04\x01\x12\x04\x06\0\n\x01\n\n\n\x03\x04\x01\x01\x12\x03\x06\x08\x1a\n\
+    \x0b\n\x04\x04\x01\x02\0\x12\x03\x07\x04\x15\n\x0c\n\x05\x04\x01\x02\0\
+    \x05\x12\x03\x07\x04\n\n\x0c\n\x05\x04\x01\x02\0\x01\x12\x03\x07\x0b\x10\
+    \n\x0c\n\x05\x04\x01\x02\0\x03\x12\x03\x07\x13\x14\n\x0b\n\x04\x04\x01\
+    \x02\x01\x12\x03\x08\x04\x1e\n\x0c\n\x05\x04\x01\x02\x01\x06\x12\x03\x08\
+    \x04\x12\n\x0c\n\x05\x04\x01\x02\x01\x01\x12\x03\x08\x13\x19\n\x0c\n\x05\
+    \x04\x01\x02\x01\x03\x12\x03\x08\x1c\x1d\n\x0b\n\x04\x04\x01\x02\x02\x12\
+    \x03\t\x04\x1e\n\x0c\n\x05\x04\x01\x02\x02\x05\x12\x03\t\x04\x08\n\x0c\n\
+    \x05\x04\x01\x02\x02\x01\x12\x03\t\t\x19\n\x0c\n\x05\x04\x01\x02\x02\x03\
+    \x12\x03\t\x1c\x1d\n\n\n\x02\x04\x02\x12\x04\x0b\0\x0e\x01\n\n\n\x03\x04\
+    \x02\x01\x12\x03\x0b\x08\x16\n\x0b\n\x04\x04\x02\x02\0\x12\x03\x0c\x04\
+    \x1d\n\x0c\n\x05\x04\x02\x02\0\x05\x12\x03\x0c\x04\n\n\x0c\n\x05\x04\x02\
+    \x02\0\x01\x12\x03\x0c\x0b\x18\n\x0c\n\x05\x04\x02\x02\0\x03\x12\x03\x0c\
+    \x1b\x1c\n\x0b\n\x04\x04\x02\x02\x01\x12\x03\r\x04\x1c\n\x0c\n\x05\x04\
+    \x02\x02\x01\x05\x12\x03\r\x04\n\n\x0c\n\x05\x04\x02\x02\x01\x01\x12\x03\
+    \r\x0b\x17\n\x0c\n\x05\x04\x02\x02\x01\x03\x12\x03\r\x1a\x1bb\x06proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 5 - 1
shared-lib/flowy-user-data-model/src/protobuf/proto/user_setting.proto

@@ -6,6 +6,10 @@ message UserPreferences {
 }
 message AppearanceSettings {
     string theme = 1;
-    string language = 2;
+    LocaleSettings locale = 2;
     bool reset_as_default = 3;
 }
+message LocaleSettings {
+    string language_code = 1;
+    string country_code = 2;
+}