diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 34e67be..75a7aa9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,16 @@ This file contains changes made to **Python Password** program. +## [0.2.5] - 2020-05-XX +### Added +- Translation option. +- Polish locale. +- Safer theme changing. +### Changed +- New icon. +### Fixed +- Two tries to access settings. + ## [0.2.4] - 2020-05-19 ### Added - Chosen theme is now saving. @@ -60,6 +70,7 @@ First usable pre-release. - Set password option. - Get password option. +[0.2.5]: https://github.com/AnonymousX86/Python-Password/releases/tag/v0.2.5-alpha [0.2.4]: https://github.com/AnonymousX86/Python-Password/releases/tag/v0.2.4-alpha [0.2.3]: https://github.com/AnonymousX86/Python-Password/releases/tag/v0.2.3-alpha [0.2.2]: https://github.com/AnonymousX86/Python-Password/releases/tag/v0.2.2-alpha diff --git a/python_password/PyPassword.py b/python_password/PyPassword.py index f049f21..e104b59 100644 --- a/python_password/PyPassword.py +++ b/python_password/PyPassword.py @@ -21,12 +21,13 @@ from python_password.utils.database import * from python_password.utils.files import * from python_password.utils.settings import * +from translations.core import * class SimpleDialog: """Simple dialogs.""" - def __init__(self, title, text, alert_text='OK', **kwargs): + def __init__(self, title, text, alert_text, **kwargs): super().__init__(**kwargs) self.title = title self.text = text @@ -71,23 +72,30 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.theme_cls.primary_palette = 'Indigo' self.theme_cls.theme_style = 'Light' - self.text_color_hex = '111111' + self.text_color_hex = get_setting('text_color_hex') self.passwords = [] self.info = { 'name': 'Python Password', - 'version': '0.2.4', + 'version': '0.2.5', 'author': 'Jakub Suchenek', 'github': 'https://github.com/AnonymousX86/Python-Password', 'faq': 'https://github.com/AnonymousX86/Python-Password/blob/master/docs/FAQ.md', 'mail': 'mailto:jakub.suchenek.25@gmail.com', 'icon': 'Icon made by Freepik from www.flaticon.com', - 'rd_party': 'UPX, Kivy and KivyMD' + '3rd_party': 'UPX, Kivy and KivyMD' } + self.all_languages = { + 'en': 'English', + 'pl': 'Polski' + } + self.current_language = get_setting('language') + self.messages = {} # It has to be outside ``__init__`` - text_color_rgba = ListProperty([.06, .06, .06, 1]) + text_color_rgba = ListProperty(get_setting('text_color_rgba')) def build(self): + self.update_messages() with open(f'kv_templates{sep}PyPassword.kv', encoding='utf8') as fd: kv = Builder.load_string(fd.read()) return kv @@ -96,6 +104,9 @@ def on_start(self): self.update_passwords_list() self.masters_ok() self.switch_theme(get_theme()) + self.update_languages_list() + # Development option + # self.root.ids.screen_manager.current = 'languages' # ================================ # Information @@ -105,12 +116,12 @@ def ctx_password(self, instance): """Shows dialog with options what to do with password.""" ctx_dialog = MDDialog( title=f'[color=#{self.text_color_hex}]{instance.text.capitalize()}[/color]', - text='Choose what do you want to do wth this password.', + text=self.tr('ctx_text'), auto_dismiss=False, buttons=[ MDRectangleFlatIconButton( icon='content-copy', - text='Copy', + text=self.tr('copy'), on_release=lambda x: [ self.copy_password(instance.text), ctx_dialog.dismiss() @@ -118,7 +129,7 @@ def ctx_password(self, instance): ), MDRectangleFlatIconButton( icon='trash-can-outline', - text='Delete', + text=self.tr('delete'), on_release=lambda x: [ self.del_password(instance.text, True), ctx_dialog.dismiss() @@ -126,7 +137,7 @@ def ctx_password(self, instance): ), MDRectangleFlatIconButton( icon='arrow-left-circle-outline', - text='Nothing', + text=self.tr('nothing'), on_release=lambda x: ctx_dialog.dismiss() ) ] @@ -136,8 +147,7 @@ def ctx_password(self, instance): def detailed_info(self, name: str): """Opens dialog about specific info about program in ``info`` screen.""" info_dialog = MDDialog( - title=f'[color={self.text_color_hex}]' + - (name.capitalize() if name != 'rd_party' else '3rd party software') + '[/color]', + title=self.tr(name, is_title=True), text=self.info[name], auto_dismiss=False, buttons=[ @@ -154,8 +164,9 @@ def detailed_info(self, name: str): def wip_info(self): info_dialog = SimpleDialog( - title='Work in progress', - text='This feature is under development.' + title=self.tr('wip_title'), + text=self.tr('wip_text'), + alert_text=self.tr('ok') ).alert() info_dialog.open() @@ -171,21 +182,24 @@ def add_password(self): ok_value = self.validate_input(value_box, 6) if ok_alias is ValueTooShort or ok_value is ValueTooShort: result_dialog = SimpleDialog( - title='Whoops!', - text='At least one value is too short.' + title=self.tr('whoops'), + text=self.tr('error_input_too_short'), + alert_text=self.tr('ok') ).alert() elif ok_alias is PatternError or ok_value is PatternError: result_dialog = SimpleDialog( - title='Whoops!', - text='At last one value is invalid.' + title=self.tr('whoops'), + text=self.tr('error_input_invalid'), + alert_text=self.tr('ok') ).alert() else: password_alias = alias_box.text.capitalize() password_value = value_box.text if already_exists(password_alias): result_dialog = SimpleDialog( - title='Whoops!', - text='That password already exists.' + title=self.tr('whoops'), + text=self.tr('error_already_exists'), + alert_text=self.tr('ok') ).alert() else: save_password( @@ -193,8 +207,9 @@ def add_password(self): encrypt(password_value) ) result_dialog = SimpleDialog( - title='Success!', - text=f'Password "{password_alias}" successfully saved.' + title=self.tr('success'), + text=self.tr('success_save', txt_format=password_alias), + alert_text=self.tr('ok') ).alert() result_dialog.open() alias_box.text = '' @@ -205,32 +220,36 @@ def copy_password(self, password=None): to_decrypt = get_one_password(password) if to_decrypt is None: result_dialog = SimpleDialog( - title='Warning!', - text='That password do not exists in database.' + title=self.tr('warning'), + text=self.tr('error_not_exists'), + alert_text=self.tr('ok') ).alert() elif type(to_decrypt) is not bytes: result_dialog = SimpleDialog( - title='Warning!', - text='An critical error has occurred. Passwords are saved to local database in wrong way.' + title=self.tr('warning'), + text=self.tr('error_bad_format'), + alert_text=self.tr('ok') ).alert() else: decrypted = decrypt(to_decrypt) if type(decrypted) is InvalidToken: result_dialog = SimpleDialog( - title='Warning!', - text='Alpha password do not match. If you want to access this password,' - ' please change alpha password. If needed, change beta password too.' + title=self.tr('warning'), + text=self.tr('error_invalid_token'), + alert_text=self.tr('ok') ).alert() elif type(decrypted) is not str: result_dialog = SimpleDialog( - title='Warning!', - text=f'An error has occurred: {decrypted}' + title=self.tr('warning'), + text=self.tr('error_unknown', txt_format=decrypted), + alert_text=self.tr('ok') ).alert() else: copy(decrypted) result_dialog = SimpleDialog( - title='Success!', - text='Password copied to clipboard. Now you can paste in somewhere.' + title=self.tr('success'), + text=self.tr('success_copy'), + alert_text=self.tr('ok') ).alert() result_dialog.open() self.update_passwords_list() @@ -241,34 +260,38 @@ def del_password(self, password=None, force=False): self.root.ids.del_password_alias.text = '' if not force and len(password) == 0: result_dialog = SimpleDialog( - title='Whoops!', - text='Please provide password alias at first.' + title=self.tr('whoops'), + text=self.tr('error_no_alias'), + alert_text=self.tr('ok') ).alert() elif not force and len(password) < 3: result_dialog = SimpleDialog( - title='Whoops!', - text='Password alias has to be at least 3 characters long.' + title=self.tr('whoops'), + text=self.tr('error_alias_too_short', txt_format='3'), + alert_text=self.tr('ok') ).alert() else: to_del = get_one_password(password) if to_del is None: result_dialog = SimpleDialog( - title='Warning!', - text='That password do not exists in database' + title=self.tr('warning'), + text=self.tr('error_not_exists'), + alert_text=self.tr('ok') ).alert() elif type(to_del) is not bytes: result_dialog = SimpleDialog( - title='Warning!', - text='An critical error has occurred. Passwords are saved to local database in wrong way.' + title=self.tr('warning'), + text=self.tr('error_bad_format'), + alert_text=self.tr('ok') ).alert() else: result_dialog = MDDialog( - title='Attention', - text=f'Do you really want to delete "{password}" password?', + title=self.tr('warning'), + text=self.tr('confirm_delete', txt_format=password), auto_dismiss=False, buttons=[ MDFillRoundFlatIconButton( - text='Yes', + text=self.tr('yes'), icon='check-circle-outline', on_release=lambda x: [ self._del_password_confirm(password), @@ -276,7 +299,7 @@ def del_password(self, password=None, force=False): ] ), MDRoundFlatIconButton( - text='No', + text=self.tr('no'), icon='close-circle-outline', on_release=lambda x: self.dismiss_and_back(result_dialog) ) @@ -287,8 +310,9 @@ def del_password(self, password=None, force=False): def _del_password_confirm(self, password: str): del_password(password) result_dialog = SimpleDialog( - title='Success!', - text=f'Password "{password}" successfully deleted.' + title=self.tr('success'), + text=self.tr('success_delete', txt_format=password), + alert_text=self.tr('ok') ).alert() result_dialog.open() self.update_passwords_list() @@ -301,18 +325,17 @@ def masters_ok(self): """Checks if master passwords are OK.""" if not check_beta(): alert_dialog = MDDialog( - title='Missing beta password', - text='I\'ve noticed, that you have not set beta password. It\'s needed for safe password storing.' - ' Do you want to provide it by yourself, or let program to randomize it?', + title=self.tr('error_missing_master_title', txt_format=self.tr('beta')), + text=self.tr('error_missing_master_text'), auto_dismiss=False, buttons=[ MDFillRoundFlatIconButton( - text='Set password', + text=self.tr('set_password'), icon='account-key-outline', on_release=lambda x: self.dismiss_and_back(alert_dialog, 'settings') ), MDRoundFlatIconButton( - text='Randomize', + text=self.tr('randomize'), icon='dice-multiple-outline', on_release=lambda x: [ self.reset_beta(), @@ -325,18 +348,17 @@ def masters_ok(self): return False elif not check_alpha(): alert_dialog = MDDialog( - title='Missing alpha password', - text='I\'ve noticed, that you have not set alpha password. It\'s needed for safe password storing.' - ' Do you want to provide it by yourself, or let program to randomize it?', + title=self.tr('error_missing_master_title', txt_format=self.tr('alpha')), + text=self.tr('error_missing_master_text'), auto_dismiss=False, buttons=[ MDFillRoundFlatIconButton( - text='Set password', + text=self.tr('set_password'), icon='account-key-outline', on_release=lambda x: self.dismiss_and_back(alert_dialog, 'settings') ), MDRoundFlatIconButton( - text='Randomize', + text=self.tr('randomize'), icon='dice-multiple-outline', on_release=lambda x: [ self.reset_alpha(), @@ -366,19 +388,20 @@ def change_master(self, which: str, preset=None): if len(password) < 6: password_box.error = True result_dialog = SimpleDialog( - title='Whoops!', - text=f'{which.capitalize()} password should be at least 6 characters long.' + title=self.tr('whoops'), + text=self.tr('error_master_too_short', txt_format=[self.tr(which), '6']), + alert_text=self.tr('ok') ).alert() else: password_box.error = False generate_func(password) result_dialog = MDDialog( - title='Success!', - text=f'New {which} password successfully saved.', + title=self.tr('success'), + text=self.tr('success_new_master', txt_format=self.tr(which)), auto_dismiss=False, buttons=[ MDRaisedButton( - text='OK', + text=self.tr('ok'), on_release=lambda x: self.dismiss_and_back(result_dialog) ) ] @@ -389,23 +412,21 @@ def change_master(self, which: str, preset=None): def reset_alpha(self): confirm_dialog = MDDialog( - title='Warning!', - text='This will randomize alpha password. It will be super safe, but you probably will not be able to' - ' re-enter this password later. You will not be able to copy already saved passwords.' - ' Do you want to continue?', + title=self.tr('warning'), + text=self.tr('confirm_randomize_alpha'), auto_dismiss=False, buttons=[ MDFillRoundFlatIconButton( + text=self.tr('yes'), icon='check-circle-outline', - text='Yes', on_release=lambda x: [ self.change_master('alpha', urandom(16)), confirm_dialog.dismiss() ] ), MDRoundFlatIconButton( + text=self.tr('no'), icon='close-circle-outline', - text='No', on_release=lambda x: confirm_dialog.dismiss() ) ] @@ -414,22 +435,21 @@ def reset_alpha(self): def reset_beta(self): confirm_dialog = MDDialog( - title='Warning!', - text='This will randomize beta password. It will be super safe, but you probably will not be able to' - ' re-enter this password later. Do you want to continue?', + title=self.tr('warning'), + text=self.tr('confirm_randomize_beta'), auto_dismiss=False, buttons=[ MDFillRoundFlatIconButton( + text=self.tr('yes'), icon='check-circle-outline', - text='Yes', on_release=lambda x: [ self.change_master('beta', urandom(16)), confirm_dialog.dismiss() ] ), MDRoundFlatIconButton( + text=self.tr('no'), icon='close-circle-outline', - text='No', on_release=lambda x: confirm_dialog.dismiss() ) ] @@ -470,27 +490,27 @@ def _set_passwords_list(self): def _set_passwords_count(self): count = len(self.passwords) if count == 0: - text = 'There are no passwords in database.' + text = self.tr('password_count_0') self.root.ids.passwords_list.add_widget( NewbieTip( - text='Welcome to Python Password', - secondary_text='Save your first password, by entering', - tertiary_text='it\'s data on the right', + text=self.tr('tip_1_title', txt_format=self.info['name']), + secondary_text=self.tr('tip_1_text_1'), + tertiary_text=self.tr('tip_1_text_2'), icon='arrow-right-bold-circle' ) ) elif count == 1: - text = 'There\'s only 1 password in database.' + text = self.tr('password_count_1') self.root.ids.passwords_list.add_widget( NewbieTip( - text='Congratulations!', - secondary_text='You have saved your\'s 1st password.', - tertiary_text='Preview it just by clicking it.', + text=self.tr('tip_2_title'), + secondary_text=self.tr('tip_2_text_1'), + tertiary_text=self.tr('tip_2_text_2'), icon='arrow-up-bold-circle' ) ) else: - text = f'There are {count} passwords in database.' + text = self.tr('passwords_count_x', txt_format=count) self.root.ids.passwords_count.text = text def update_passwords_list(self): @@ -498,10 +518,83 @@ def update_passwords_list(self): self._set_passwords_list() self._set_passwords_count() + # ================================ + # Languages + # ================================ + + def _set_languages_list(self): + self.root.ids.languages_list.clear_widgets() + for lang in self.all_languages.keys(): + self.root.ids.languages_list.add_widget( + OneLineListItem( + text=self.all_languages[lang], + on_release=lambda x: self.change_language( + list(self.all_languages.keys())[list(self.all_languages.values()).index(x.text)] + ) + ) + ) + + def update_languages_list(self): + self._set_languages_list() + + def update_messages(self): + self.messages = get_messages(get_setting('language')) + + def change_language(self, lang: str): + if not check_language(lang): + result_dialog = MDDialog( + title=self.tr('whoops'), + text=self.tr('error_locale_missing'), + auto_dismiss=False, + buttons=[ + MDRaisedButton( + text=self.tr('yes'), + on_release=lambda x: [ + download_locale(lang), + set_locale(lang), + self.restart_required(), + result_dialog.dismiss() + ] + ), + MDRaisedButton( + text=self.tr('no'), + on_release=lambda x: result_dialog.dismiss() + ) + ] + ) + result_dialog.open() + else: + set_locale(lang) + self.restart_required() + + def add_language(self): + self.open_url( + 'https://github.com/AnonymousX86/Python-Password/blob/master/docs/CONTRIBUTING.md#adding-new-locales' + ) + + def tr(self, text_id, txt_format=None, is_title=False): + try: + result = self.messages[text_id] + except KeyError: + try: + result = default_messages[text_id] + except KeyError: + return f'Missing: {text_id}' + if txt_format: + result = result.format(txt_format) + if is_title: + result = f'[color=#{get_setting("text_color_hex")}]{result}[/color]' + return result + # ================================ # Misc # ================================ + def update_theme(self): + self.text_color_hex = get_setting('text_color_hex') + self.text_color_rgba = get_setting('text_color_rgba') + self.theme_cls.theme_style = get_setting('theme') + def switch_theme(self, force=None): """ Changes theme to opposite or forced. @@ -517,17 +610,23 @@ def switch_theme(self, force=None): if current == 'Light': self.theme_cls.theme_style = 'Dark' - self.text_color_hex = 'ffffff' - self.text_color_rgba = (1, 1, 1, 1) set_theme('Dark') elif current == 'Dark': self.theme_cls.theme_style = 'Light' - self.text_color_hex = '111111' - self.text_color_rgba = (.06, .06, .06, 1) set_theme('Light') else: raise NameError('No theme found') + self.update_theme() + + def restart_required(self): + result_dialog = SimpleDialog( + title=self.tr('success'), + text=self.tr('restart_required'), + alert_text=self.tr('ok') + ).alert() + result_dialog.open() + def open_url(self, url: str): """Opens URL in default browser.""" open_new_tab(url) @@ -545,7 +644,10 @@ def validate_input(self, instance, length: int): if len(instance.text) < length: instance.error = True return ValueTooShort - elif match('^[A-Za-z0-9][A-Za-z0-9 &\\-_]+[A-Za-z0-9]$', instance.text) is None: + elif match( + '^[A-Za-zĘÓĄŚŁŻŹĆŃęóąśłżźćń0-9][A-Za-z0-9ĘÓĄŚŁŻŹĆŃęóąśłżźćń &\\-_]+[A-Za-z0-9ĘÓĄŚŁŻŹĆŃęóąśłżźćń]$', + instance.text + ) is None: instance.error = True return PatternError else: diff --git a/python_password/kv_templates/PyPassword.kv b/python_password/kv_templates/PyPassword.kv index 962f32c..60d223f 100644 --- a/python_password/kv_templates/PyPassword.kv +++ b/python_password/kv_templates/PyPassword.kv @@ -4,7 +4,7 @@ spacing: 15 MDLabel: - text: 'Python Password' + text: app.info['name'] font_style: 'Button' theme_text_color: 'Primary' size_hint_y: None @@ -25,25 +25,34 @@ OneLineIconListItem: icon_color: 'black' - text: 'Passwords' + text: app.tr('passwords') on_press: root.change_screen('passwords') IconLeftWidget: icon: 'shield-lock-outline' OneLineIconListItem: - text: 'Settings' + text: app.tr('settings') on_press: root.change_screen('settings') IconLeftWidget: icon: 'cogs' OneLineIconListItem: - text: 'Info' + text: app.tr('info') on_press: root.change_screen('info') IconLeftWidget: icon: 'information-outline' MDRoundFlatIconButton: - text: 'Switch theme' + text: app.tr('language') + size_hint_x: 1 + background_palette: 'Primary' + text_color: app.text_color_rgba + icon: 'flag-outline' + on_release: root.change_screen('languages') + + MDRoundFlatIconButton: + text: app.tr('switch_theme') + size_hint_x: 1 background_palette: 'Primary' text_color: app.text_color_rgba icon: 'theme-light-dark' @@ -56,7 +65,7 @@ Screen: MDToolbar: id: toolbar - title: 'Python Password' + title: app.info['name'] pos_hint: {'top': 1} elevation: 10 left_action_items: [['menu', lambda x: nav_drawer.set_state('open')]] @@ -94,8 +103,8 @@ Screen: MDTextField: id: password_alias - hint_text: 'Password alias' - helper_text: '3+ characters long.' + hint_text: app.tr('password_alias') + helper_text: app.tr('x_characters_long', txt_format='3') helper_text_mode: 'on_error' mode: 'rectangle' required: False @@ -104,8 +113,8 @@ Screen: MDTextField: id: password_value - hint_text: 'Password value' - helper_text: '6+ characters long.' + hint_text: app.tr('password_value') + helper_text: app.tr('x_characters_long', txt_format='6') helper_text_mode: 'on_error' mode: 'rectangle' required: False @@ -114,7 +123,7 @@ Screen: MDFillRoundFlatIconButton: size_hint_x: 1 - text: 'Add password' + text: app.tr('save_password') icon: 'key-plus' on_press: app.add_password() @@ -122,8 +131,8 @@ Screen: MDTextField: id: del_password_alias - hint_text: 'Password alias' - helper_text: '3+ characters long.' + hint_text: app.tr('password_alias') + helper_text: app.tr('x_characters_long', txt_format='3') helper_text_mode: 'on_error' mode: 'rectangle' required: False @@ -132,7 +141,7 @@ Screen: MDFillRoundFlatIconButton: size_hint_x: 1 - text: 'Remove password' + text: app.tr('remove_password') icon: 'key-minus' on_release: app.del_password() @@ -149,7 +158,7 @@ Screen: MDFillRoundFlatIconButton: size_hint_x: 1 - text: 'Refresh' + text: app.tr('refresh') icon: 'refresh' on_release: app.update_passwords_list() @@ -167,7 +176,7 @@ Screen: BoxLayout: MDLabel: - text: 'Change alpha password' + text: app.tr('change_master', txt_format=app.tr('alpha')) theme_text_color: 'Primary' halign: 'center' @@ -177,8 +186,8 @@ Screen: MDTextField: id: alpha_change - hint_text: 'New alpha password' - helper_text: '6+ characters long.' + hint_text: app.tr('new_master', txt_format=app.tr('alpha')) + helper_text: app.tr('x_characters_long', txt_format='6') helper_text_mode: 'on_error' mode: 'rectangle' required: False @@ -192,13 +201,13 @@ Screen: MDFillRoundFlatIconButton: id: btn_alpha_change - text: 'Save' + text: app.tr('save') icon: 'checkbox-marked-outline' on_release: app.change_master('alpha') MDFillRoundFlatIconButton: id: btn_alpha_reset - text: 'Reset' + text: app.tr('reset') icon: 'lock-reset' on_release: app.reset_alpha() @@ -207,7 +216,7 @@ Screen: BoxLayout: MDLabel: - text: 'Change beta password' + text: app.tr('change_master', txt_format=app.tr('beta')) theme_text_color: 'Primary' halign: 'center' @@ -217,8 +226,8 @@ Screen: MDTextField: id: beta_change - hint_text: 'New beta password' - helper_text: '6+ characters long.' + hint_text: app.tr('new_master', txt_format=app.tr('beta')) + helper_text: app.tr('x_characters_long', txt_format='6') helper_text_mode: 'on_error' mode: 'rectangle' required: False @@ -231,12 +240,12 @@ Screen: spacing: 15 MDFillRoundFlatIconButton: - text: 'Save' + text: app.tr('save') icon: 'checkbox-marked-outline' on_release: app.change_master('beta') MDFillRoundFlatIconButton: - text: 'Reset' + text: app.tr('reset') icon: 'lock-reset' on_release: app.reset_beta() @@ -249,12 +258,12 @@ Screen: Widget: MDFillRoundFlatIconButton: - text: 'Export backup' + text: app.tr('export_backup') icon: 'database-export' on_release: app.backup_export() MDFillRoundFlatIconButton: - text: 'Import backup' + text: app.tr('import_backup') icon: 'database-import' on_release: app.backup_import() @@ -276,7 +285,7 @@ Screen: orientation: 'vertical' MDLabel: - text: 'About the program' + text: app.tr('about') theme_text_color: 'Primary' size_hint: (1, None) height: '30dp' @@ -287,34 +296,34 @@ Screen: TwoLineListItem: text: app.info['name'] - secondary_text: 'Name' + secondary_text: app.tr('name') on_release: app.detailed_info('name') TwoLineListItem: text: app.info['version'] - secondary_text: 'Version' + secondary_text: app.tr('version') on_release: app.detailed_info('version') TwoLineListItem: text: app.info['author'] - secondary_text: 'Author' + secondary_text: app.tr('author') on_release: app.detailed_info('author') TwoLineListItem: text: app.info['icon'] - secondary_text: 'Program icon' + secondary_text: app.tr('icon') on_release: app.detailed_info('icon') TwoLineListItem: - text: app.info['rd_party'] - secondary_text: '3rd party software' - on_release: app.detailed_info('rd_party') + text: app.info['3rd_party'] + secondary_text: app.tr('3rd_party') + on_release: app.detailed_info('3rd_party') BoxLayout: orientation: 'vertical' MDLabel: - text: 'Useful links' + text: app.tr('links') theme_text_color: 'Primary' size_hint: (1, None) height: '30dp' @@ -324,7 +333,7 @@ Screen: MDList: TwoLineAvatarListItem: - text: 'GitHub repository' + text: app.tr('github') secondary_text: ' ' on_release: app.open_url(app.info['github']) @@ -332,7 +341,7 @@ Screen: icon: 'github-circle' TwoLineAvatarListItem: - text: 'FAQ' + text: app.tr('faq') secondary_text: ' ' on_release: app.open_url(app.info['faq']) @@ -340,13 +349,44 @@ Screen: icon: 'comment-question-outline' TwoLineAvatarIconListItem: - text: 'Mail' + text: app.tr('mail') secondary_text: ' ' on_release: app.open_url(app.info['mail']) IconLeftWidget: icon: 'email-outline' + Screen: + name: 'languages' + + BoxLayout: + size_hint: (1, None) + height: root.height - toolbar.height + orientation: 'vertical' + spacing: 30 + padding: (150, 45) + + MDLabel: + text: app.tr('available_languages') + theme_text_color: 'Primary' + size_hint_y: None + halign: 'center' + valign: 'middle' + height: 14 + + MDSeparator: + + ScrollView: + MDList: + id: languages_list + + MDSeparator: + + MDFillRoundFlatIconButton: + text: app.tr('add_language') + icon: 'flag-plus-outline' + on_release: app.add_language() + MDNavigationDrawer: md_bg_color: app.theme_cls.bg_dark id: nav_drawer diff --git a/python_password/translations/__init__.py b/python_password/translations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_password/translations/core.py b/python_password/translations/core.py new file mode 100644 index 0000000..cad08c1 --- /dev/null +++ b/python_password/translations/core.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +from json import load, dump +from os import sep, mkdir +from pathlib import Path + +from requests import get + +from utils.files import appdata +from utils.settings import set_setting + + +default_messages = { + "__info__": { + "language": "en", + "compatible_version": "0.2.5", + }, + "alpha": "alfa", + "beta": "beta", + "passwords": "Passwords", + "settings": "Settings", + "info": "Info", + "language": "Language", + "switch_theme": "Switch theme", + "password_alias": "Password alias", + "password_value": "Password value", + "x_characters_long": "{0}+ characters long.", + "save_password": "Save password", + "remove_password": "Remove password", + "password_count_0": "There are no passwords in database.", + "passwords_count_1": "There's only 1 password in database.", + "passwords_count_x": "There are {0} passwords in database.", + "refresh": "Refresh", + "change_master": "Change {0} password", + "new_master": "New {0} password", + "save": "Save", + "reset": "Reset", + "export_backup": "Export backup", + "import_backup": "Import backup", + "about": "About the program", + "name": "Name", + "version": "Version", + "author": "Author", + "icon": "Program icon", + "3rd_party": "3rd party software", + "links": "Useful links", + "github": "GitHub repository", + "faq": "FAQ", + "mail": "Mail", + "available_languages": "Available languages", + "add_language": "Add language", + "warning": "Warning!", + "whoops": "Whoops!", + "success": "Success!", + "ok": "OK", + "yes": "Yes", + "no": "No", + "wip_title": "Work in progress", + "wip_text": "This feature is under development.", + "ctx_text": "Choose what do you want to do wth this password.", + "copy": "Copy", + "delete": "Delete", + "nothing": "Nothing", + "success_save": "Password \"{0}\" successfully saved.", + "success_copy": "Password copied to clipboard. Now you can paste in somewhere.", + "success_delete": "Password \"{0}\" successfully deleted.", + "success_new_master": "New {0} passwords successfully saved.", + "confirm_delete": "Do you really want to delete \"{0}\" password?", + "confirm_randomize_alpha": "This will randomize alpha password. It will be super safe, but you probably will not be" + " able to re-enter this password later. You will not be able to access already saved" + " passwords. Do you want to continue?", + "confirm_randomize_beta": "This will randomize beta password. It will be super safe, but you probably will not be" + " able to re-enter this password later. Do you want to continue?", + "error_bad_format": "An critical error has occurred. Passwords are saved to local database in wrong way.", + "error_invalid_token": "Alpha password do not match. If you want to access this password, please change alpha" + " password. If needed, change beta password too.", + "error_no_alias": "Please provide password alias at first", + "error_alias_too_short": "Password alias has to be at least {0} characters long", + "error_already_exists": "That password already exists in database.", + "error_not_exists": "That password do not exists in database.", + "error_missing_master_title": "Missing {0} password", + "error_missing_master_text": "It's needed for safe password storing. Do you want to provide it by yourself, or let" + " program to randomize it?", + "error_master_too_short": "{0} password should be at least {1} characters long.", + "error_input_too_short": "At least one value is too short.", + "error_input_invalid": "At last one value is invalid.", + "error_locale_missing": "This language is not found, do you want to download it?", + "error_unknown": "An error has occurred: {0}", + "set_password": "Set password", + "randomize": "Randomize", + "tip_1_title": "Welcome to {0}", + "tip_1_text_1": "Save your first password, by entering", + "tip_1_text_2": "it's data on the right.", + "tip_2_title": "Congratulations!", + "tip_2_text_1": "You have saved your's 1st password.", + "tip_2_text_2": "Preview it just by clicking it.", + "restart_required": "To save changes, please restart app." +} +default_language = default_messages['__info__']['language'] +compatible_version = default_messages['__info__']['compatible_version'] + + +def get_messages(lang: str): + try: + # Try load preferred language + open(json_appdata(lang)) + except FileNotFoundError: + set_locale(default_language) + try: + # Try load default language + open(json_appdata(default_language)) + except FileNotFoundError: + # Load messages from Python scope + messages = default_messages + save_default_messages() + else: + # Load default language + with open(json_appdata(default_language), encoding='utf-8') as f: + messages = load(f) + else: + # Load preferred language + with open(json_appdata(lang), encoding='utf-8') as f: + messages = load(f) + return messages + + +def check_language(name: str): + try: + open(json_appdata(name)) + except FileNotFoundError: + return False + else: + return True + + +def save_default_messages(): + try: + open(json_appdata(default_language)) + except FileNotFoundError: + create_locales_directory() + open(json_appdata(default_language), 'x') + with open(json_appdata(default_language), 'w') as f: + dump(default_messages, f) + + +def download_locale(name: str): + create_locales_directory() + source = f'https://raw.githubusercontent.com/AnonymousX86/Python-Password/master/' \ + f'python_password/translations/locales/{name}.json' + locale = get(source) + with open(json_appdata(name), 'wb') as f: + f.write(locale.content) + + +def json_appdata(lang: str): + return appdata(f'locales{sep}{lang}.json') + + +def _json_locales(lang: str): + return f'{Path().absolute()}{sep}locales{sep}{lang}.json' + + +def create_locales_directory(): + try: + mkdir(appdata('locales')) + except FileExistsError: + pass + + +def set_locale(lang: str): + set_setting('language', lang) + + +def test_locales(lang_list=None): + if lang_list is None: + lang_list = ['en', 'pl'] + for lang in lang_list: + with open(_json_locales(lang), encoding='utf-8') as f: + json_data = load(f) + assert len(default_messages)-1 == len(json_data), f'Not all keys are included in "{lang.upper()}.JSON"' + for key in default_messages.keys(): + if key != '__info__': + try: + default_value = default_messages[key] + except KeyError: + default_value = None + assert default_value is not None, f'Key "{key}" not found in default values' + try: + json_value = json_data[key] + except KeyError: + json_value = None + assert json_value is not None, f'Key "{key}" not found in "{lang.upper()}.JSON" values' diff --git a/python_password/utils/settings.py b/python_password/utils/settings.py index 940b1a7..ac06f4f 100644 --- a/python_password/utils/settings.py +++ b/python_password/utils/settings.py @@ -5,7 +5,10 @@ default_settings = { - 'theme': 'Light' + 'theme': 'Light', + 'text_color_hex': '111111', + 'text_color_rgba': [.06, .06, .06, 1], + 'language': 'en' } @@ -36,7 +39,7 @@ def set_setting(name: str, value): dump(settings, f) -def get_setting(name: str): +def get_setting(name: str, second_try=False): try: open(appdata(Files.settings)) except FileNotFoundError: @@ -46,14 +49,26 @@ def get_setting(name: str): settings = load(f) try: result = settings[name] - except IndexError: - return IndexError('Missing setting') + except KeyError: + if not second_try: + reset_settings() + return get_setting(name, second_try=True) + else: + return KeyError('Missing setting') else: return result def set_theme(theme: str): set_setting('theme', theme) + if theme == 'Dark': + set_setting('text_color_hex', 'ffffff') + set_setting('text_color_rgba', [1, 1, 1, 1]) + elif theme == 'Light': + set_setting('text_color_hex', '111111') + set_setting('text_color_rgba', [.06, .06, .06, 1]) + else: + raise NameError('No theme found') def get_theme(): diff --git a/requirements.txt b/requirements.txt index 1163cae..c2224b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pyperclip~=1.8.0 kivy~=1.11.1 kivymd~=0.104.1 cryptography~=2.9.2 +requests~=2.23.0 diff --git a/setup.py b/setup.py index c1cb1fc..165fb8d 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='Python Password', - version='0.2.4', + version='0.2.5', description='Simple password storing app.', long_description=long_description, long_description_content_type='text/markdown',