From d88e6e476b70ee26b1a14a7f0a4b3a1349f1d60d Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 1 Mar 2024 10:16:44 +0100 Subject: [PATCH] use gettext to translate the project doesn't include any other languages, but this commit allows the application to support multiple languages --- README.md | 8 + data/meson.build | 21 +- flatpak/icu.ccw.Melon.yml | 10 +- melon/browse/__init__.py | 15 +- melon/browse/channel.py | 11 +- melon/browse/playlist.py | 5 +- melon/browse/search.py | 25 +- melon/browse/server.py | 21 +- melon/home/history.py | 13 +- melon/home/new.py | 19 +- melon/home/playlists.py | 15 +- melon/home/subs.py | 13 +- melon/import_providers/newpipe.py | 9 +- melon/importer.py | 13 +- melon/player/__init__.py | 12 +- melon/playlist/__init__.py | 10 +- melon/playlist/create.py | 21 +- melon/playlist/pick.py | 17 +- melon/servers/invidious/__init__.py | 18 +- melon/servers/nebula/__init__.py | 34 +- melon/servers/peertube/__init__.py | 30 +- melon/servers/utils.py | 2 - melon/settings/__init__.py | 32 +- melon/widgets/preferencerow.py | 35 +- melon/window.py | 5 +- meson.build | 2 + po/LINGUAS | 1 + po/POTFILES.in | 22 ++ po/melon.pot | 536 ++++++++++++++++++++++++++++ po/meson.build | 4 + po/update_potfiles.sh | 36 ++ 31 files changed, 834 insertions(+), 181 deletions(-) create mode 100644 po/LINGUAS create mode 100644 po/POTFILES.in create mode 100644 po/melon.pot create mode 100644 po/meson.build create mode 100755 po/update_potfiles.sh diff --git a/README.md b/README.md index 80276c6..f5a3b10 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,17 @@ Of course, you are welcome to send issues and pull requests via email as well: - `py3-requests` - `py3-unidecode` - `gtk4.0` +- `gettext` - `libadwaita` - `webkit2gtk-6.0` +## Translations +Melon supports multiple languages. +If you want to help translate Melon into your language, +head over to [Weblate](https://translate.codeberg.org/engage/melon/) + +[![Translation status](https://translate.codeberg.org/widget/melon/multi-auto.svg)](https://translate.codeberg.org/engage/melon/) + ## ⚠️ Disclaimer - The developers of this application does not have any affiliation with the content providers available. diff --git a/data/meson.build b/data/meson.build index 71615ba..ea19b47 100644 --- a/data/meson.build +++ b/data/meson.build @@ -5,6 +5,7 @@ install_data ( ) gnome = import('gnome') +i18n = import('i18n') # # .desktop file @@ -19,12 +20,30 @@ desktop_conf.set('projectname', meson.project_name()) desktop_file = configure_file( input: base_id + '.desktop.in', - output: app_id + '.desktop', + output: app_id + '.desktop.i18n.in', configuration: desktop_conf, +) + +# Merges translations +i18n.merge_file( + input: desktop_file, + output: app_id + '.desktop', + po_dir: '../po', + type: 'desktop', install: true, install_dir: join_paths(datadir, 'applications') ) +# Validating the .desktop file +desktop_file_validate = find_program('desktop-file-validate', required:false) +if desktop_file_validate.found() +test ( + 'Validate desktop file', + desktop_file_validate, + args: join_paths(meson.current_build_dir (), app_id + '.desktop') +) +endif + # # Dependencies # diff --git a/flatpak/icu.ccw.Melon.yml b/flatpak/icu.ccw.Melon.yml index 4de5575..a6e670c 100644 --- a/flatpak/icu.ccw.Melon.yml +++ b/flatpak/icu.ccw.Melon.yml @@ -32,8 +32,8 @@ modules: buildsystem: meson sources: # uncomment to build local version instead - # - type: dir - # path: ".." + - type: dir + path: ".." # # uncomment to build a specific version # - type: git @@ -41,6 +41,6 @@ modules: # tag: "0.1.2" # # build latest main branch - - type: git - url: https://codeberg.org/comcloudway/melon - branch: main + # - type: git + # url: https://codeberg.org/comcloudway/melon + # branch: main diff --git a/melon/browse/__init__.py b/melon/browse/__init__.py index 70968eb..09ba492 100644 --- a/melon/browse/__init__.py +++ b/melon/browse/__init__.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib +from gettext import gettext as _ from melon.widgets.iconbutton import IconButton from melon.servers.utils import get_allowed_servers_list @@ -14,10 +15,10 @@ class BrowseScreen(Adw.NavigationPage): if len(servers) == 0: # if no servers are allowed, show the nothing here box status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("There are no available servers") + status.set_title(_("*crickets chirping*")) + status.set_description(_("There are no available servers")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic") + icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -25,8 +26,8 @@ class BrowseScreen(Adw.NavigationPage): self.scrollview.set_child(status) else: results = Adw.PreferencesGroup() - results.set_title("Available Servers") - results.set_description("You can enable/disable and filter servers in the settings menu") + results.set_title(_("Available Servers")) + results.set_description(_("You can enable/disable and filter servers in the settings menu")) for server in servers: row = Adw.ActionRow() row.set_title(server["name"]) @@ -44,7 +45,7 @@ class BrowseScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("Servers") + self.set_title(_("Servers")) self.header_bar = Adw.HeaderBar() @@ -52,7 +53,7 @@ class BrowseScreen(Adw.NavigationPage): self.toolbar_view.add_top_bar(self.header_bar) # open global search screen search_button = IconButton("", "system-search-symbolic") - search_button.set_tooltip_text("Global Search") + search_button.set_tooltip_text(_("Global Search")) search_button.set_action_name("win.global_search") self.header_bar.pack_end(search_button) diff --git a/melon/browse/channel.py b/melon/browse/channel.py index bb4658c..6f78a25 100644 --- a/melon/browse/channel.py +++ b/melon/browse/channel.py @@ -3,6 +3,8 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib +from unidecode import unidecode +from gettext import gettext as _ from melon.servers.utils import get_server_instance, get_servers_list from melon.servers.utils import pixbuf_from_url @@ -12,7 +14,6 @@ from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preferenc from melon.widgets.iconbutton import IconButton from melon.models import get_app_settings from melon.models import is_subscribed_to_channel, ensure_subscribed_to_channel, ensure_unsubscribed_from_channel -from unidecode import unidecode class BrowseChannelScreen(Adw.NavigationPage): def fetch_page(self, page=1): @@ -95,8 +96,8 @@ class BrowseChannelScreen(Adw.NavigationPage): # add (un)subscribe button sub_pref = Preference( "subscribe", - "Subscribe to channel", - "Add latest uploads to home feed", + _("Subscribe to channel"), + _("Add latest uploads to home feed"), PreferenceType.TOGGLE, False, is_subscribed_to_channel(self.channel.server, self.channel_id) @@ -123,8 +124,8 @@ class BrowseChannelScreen(Adw.NavigationPage): break pref = Preference( "channel-feed", - "Channel feed", - "This channel provides multiple feeds, choose one to view", + _("Channel feed"), + _("This channel provides multiple feeds, choose which one to view"), PreferenceType.DROPDOWN, [ feed.name for feed in feeds ], default_name diff --git a/melon/browse/playlist.py b/melon/browse/playlist.py index 96031f8..305a202 100644 --- a/melon/browse/playlist.py +++ b/melon/browse/playlist.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib from unidecode import unidecode +from gettext import gettext as _ from melon.servers.utils import get_server_instance, get_servers_list from melon.servers.utils import pixbuf_from_url @@ -57,8 +58,8 @@ class BrowsePlaylistScreen(Adw.NavigationPage): bookmark_pref = Preference( "bookmark-playlist", - "Bookmark", - "Add Playlist to your local playlist collection", + _("Bookmark"), + _("Add Playlist to your local playlist collection"), PreferenceType.TOGGLE, False, has_bookmarked_external_playlist(server_id, playlist_id)) diff --git a/melon/browse/search.py b/melon/browse/search.py index 6d36688..e9b66ca 100644 --- a/melon/browse/search.py +++ b/melon/browse/search.py @@ -3,6 +3,8 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib +from gettext import gettext as _ +from gettext import ngettext from melon.widgets.iconbutton import IconButton from melon.widgets.feeditem import AdaptiveFeedItem @@ -39,12 +41,9 @@ class GlobalSearchScreen(Adw.NavigationPage): results = instance.search(self.text, self.search_mode) box = Adw.ExpanderRow() box.set_title(instance.name) - box.set_subtitle("No results") + box.set_subtitle(_("No results")) count = len(results) - if count == 1: - box.set_subtitle("1 result") - elif count > 1: - box.set_subtitle(f"{count} results") + box.set_subtitle(ngettext("{count} result","{count} results", count).format(count = count)) self.results.add(box) for entry in results: row = AdaptiveFeedItem(entry, app_conf["show_images_in_browse"]) @@ -56,10 +55,10 @@ class GlobalSearchScreen(Adw.NavigationPage): if len(servers) == 0: # if no servers are allowed, show the nothing here box status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("There are no available servers, a search would yield no results") + status.set_title(_("*crickets chirping*")) + status.set_description(_("There are no available servers, a search would yield no results")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic") + icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -81,15 +80,15 @@ class GlobalSearchScreen(Adw.NavigationPage): filter_popover = Gtk.Popover() filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # the filter options - filter_any = FilterButton("Any", SearchMode.ANY, self.on_filter, self.search_mode) + filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode) filter_box.append(filter_any) - filter_channels = FilterButton("Channels", SearchMode.CHANNELS, self.on_filter, self.search_mode) + filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode) filter_channels.set_group(filter_any) filter_box.append(filter_channels) - filter_playlists = FilterButton("Playlists", SearchMode.PLAYLISTS, self.on_filter, self.search_mode) + filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode) filter_playlists.set_group(filter_any) filter_box.append(filter_playlists) - filter_videos = FilterButton("Videos", SearchMode.VIDEOS, self.on_filter, self.search_mode) + filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode) filter_videos.set_group(filter_any) filter_box.append(filter_videos) @@ -104,7 +103,7 @@ class GlobalSearchScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("Global Search") + self.set_title(_("Global Search")) self.header_bar = Adw.HeaderBar() diff --git a/melon/browse/server.py b/melon/browse/server.py index 603ecaa..51f224e 100644 --- a/melon/browse/server.py +++ b/melon/browse/server.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, Gdk, GLib import threading +from gettext import gettext as _ from melon.servers.utils import get_servers_list, get_server_instance from melon.servers import SearchMode @@ -19,7 +20,7 @@ class Search(ViewStackPage): search_mode = SearchMode.ANY results = None def __init__(self, plugin): - super().__init__("search", "Search", "x-office-address-book-symbolic") + super().__init__("search", _("Search"), "x-office-address-book-symbolic") self.instance = plugin self.widget = Adw.Clamp() self.scroll = Gtk.ScrolledWindow() @@ -39,15 +40,15 @@ class Search(ViewStackPage): filter_popover = Gtk.Popover() filter_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) # the filter options - filter_any = FilterButton("Any", SearchMode.ANY, self.on_filter, self.search_mode) + filter_any = FilterButton(_("Any"), SearchMode.ANY, self.on_filter, self.search_mode) filter_box.append(filter_any) - filter_channels = FilterButton("Channels", SearchMode.CHANNELS, self.on_filter, self.search_mode) + filter_channels = FilterButton(_("Channels"), SearchMode.CHANNELS, self.on_filter, self.search_mode) filter_channels.set_group(filter_any) filter_box.append(filter_channels) - filter_playlists = FilterButton("Playlists", SearchMode.PLAYLISTS, self.on_filter, self.search_mode) + filter_playlists = FilterButton(_("Playlists"), SearchMode.PLAYLISTS, self.on_filter, self.search_mode) filter_playlists.set_group(filter_any) filter_box.append(filter_playlists) - filter_videos = FilterButton("Videos", SearchMode.VIDEOS, self.on_filter, self.search_mode) + filter_videos = FilterButton(_("Videos"), SearchMode.VIDEOS, self.on_filter, self.search_mode) filter_videos.set_group(filter_any) filter_box.append(filter_videos) @@ -97,10 +98,10 @@ class Search(ViewStackPage): if not self.results is None: self.inner.remove(self.results) self.results = Adw.StatusPage() - self.results.set_title("*crickets chirping*") - desc = "Try searching for a term" + self.results.set_title(_("*crickets chirping*")) + desc = _("Try searching for a term") if self.query != "": - desc = "Try using a different query" + desc = _("Try using a different query") self.query = "" self.results.set_description(desc) self.results.set_icon_name("weather-few-clouds-night-symbolic") @@ -121,8 +122,8 @@ class Feed(ViewStackPage): if len(results) == 0: # show empty feed page status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("This feed is empty") + status.set_title(_("*crickets chirping*")) + status.set_description(_("This feed is empty")) status.set_icon_name("weather-few-clouds-night-symbolic") self.inner.set_child(status) else: diff --git a/melon/home/history.py b/melon/home/history.py index 40bf808..a14679a 100644 --- a/melon/home/history.py +++ b/melon/home/history.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw import threading +from gettext import gettext as _ from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -23,10 +24,10 @@ class History(ViewStackPage): self.data = list(group_by_date(hist).items()) if len(hist) == 0: status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("You haven't watched any videos yet") + status.set_title(_("*crickets chirping*")) + status.set_description(_("You haven't watched any videos yet")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Browse Servers", "list-add-symbolic") + icon_button = IconButton(_("Browse Servers"), "list-add-symbolic") icon_button.set_action_name("win.browse") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -35,8 +36,8 @@ class History(ViewStackPage): else: self.results = Adw.PreferencesPage() title = Adw.PreferencesGroup() - title.set_title("History") - title.set_description("These are the videos you opened in the past") + title.set_title(_("History")) + title.set_description(_("These are the videos you opened in the past")) self.results.add(title) self.inner.set_child(self.results) self.load_page(0) @@ -78,7 +79,7 @@ class History(ViewStackPage): self.thread.start() def __init__(self): - super().__init__("history", "History", "media-playlist-repeat-symbolic") + super().__init__("history", _("History"), "media-playlist-repeat-symbolic") self.widget = Adw.Clamp() self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) diff --git a/melon/home/new.py b/melon/home/new.py index d7262f2..30886ba 100644 --- a/melon/home/new.py +++ b/melon/home/new.py @@ -5,6 +5,7 @@ gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, GLib, Gio, GObject import threading from datetime import datetime +from gettext import gettext as _ from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -17,20 +18,20 @@ from melon.models import get_cached_feed, clear_cached_feed, update_cached_feed, class NewFeed(ViewStackPage): def update(self, news): # both subscreens need the refresh button - refresh_btn = IconButton("Refresh", "view-refresh-symbolic") + refresh_btn = IconButton(_("Refresh"), "view-refresh-symbolic") refresh_btn.connect("clicked", lambda a: self.reload()) if not news: # empty home feed status = Adw.StatusPage() - status.set_title("*crickets chirping*") + status.set_title(_("*crickets chirping*")) subs = get_subscribed_channels() if len(subs) == 0: - status.set_description("Subscribe to a channel first, to view new uploads") + status.set_description(_("Subscribe to a channel first, to view new uploads")) else: - status.set_description("The channels you are subscribed to haven't uploaded anything yet") + status.set_description(_("The channels you are subscribed to haven't uploaded anything yet")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Browse Servers", "list-add-symbolic") + icon_button = IconButton(_("Browse Servers"), "list-add-symbolic") icon_button.set_action_name("win.browse") icon_button.set_margin_bottom(6) box = Gtk.CenterBox() @@ -49,10 +50,10 @@ class NewFeed(ViewStackPage): last_refresh = "Never" else: last_refresh = datetime.fromtimestamp(last_refresh).strftime("%c") - refresh_info = f" (Last refresh: {last_refresh})" + refresh_info = _("(Last refresh: {last_refresh})").format(last_refresh = last_refresh) title = Adw.PreferencesGroup() - title.set_title("What's new" + refresh_info) - title.set_description("These are the latest videos of channels you follow") + title.set_title(_("What's new") + " " + refresh_info) + title.set_description(_("These are the latest videos of channels you follow")) title.set_header_suffix(refresh_btn) results.add(title) self.inner.set_child(results) @@ -104,7 +105,7 @@ class NewFeed(ViewStackPage): self.do_update() def __init__(self): - super().__init__("feed-new", "What's new", "user-home-symbolic") + super().__init__("feed-new", _("What's new"), "user-home-symbolic") self.widget = Adw.Clamp() self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) diff --git a/melon/home/playlists.py b/melon/home/playlists.py index 2138526..2186243 100644 --- a/melon/home/playlists.py +++ b/melon/home/playlists.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw import threading +from gettext import gettext as _ from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -21,10 +22,10 @@ class Playlists(ViewStackPage): playlists.sort(key=lambda x:x.inner.title) if len(playlists) == 0: status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("You don't have any playlists yet") + status.set_title(_("*crickets chirping*")) + status.set_description(_("You don't have any playlists yet")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Create a new playlist", "list-add-symbolic") + icon_button = IconButton(_("Create a new playlist"), "list-add-symbolic") icon_button.set_action_name("win.new_playlist") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -32,9 +33,9 @@ class Playlists(ViewStackPage): self.inner.set_child(status) else: results = Adw.PreferencesGroup() - results.set_title("Playlists") - results.set_description("Here are playlists you've bookmarked or created yourself") - icon_button = IconButton("New", "list-add-symbolic", tooltip="Create a new playlist") + results.set_title(_("Playlists")) + results.set_description(_("Here are playlists you've bookmarked or created yourself")) + icon_button = IconButton(_("New"), "list-add-symbolic", tooltip=_("Create a new playlist")) icon_button.set_action_name("win.new_playlist") results.set_header_suffix(icon_button) self.inner.set_child(results) @@ -65,7 +66,7 @@ class Playlists(ViewStackPage): self.thread.start() def __init__(self): - super().__init__("playlists", "Playlists", "user-bookmarks-symbolic") + super().__init__("playlists", _("Playlists"), "user-bookmarks-symbolic") self.widget = Adw.Clamp() self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) diff --git a/melon/home/subs.py b/melon/home/subs.py index 5004dd1..26fce3b 100644 --- a/melon/home/subs.py +++ b/melon/home/subs.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw import threading +from gettext import gettext as _ from melon.widgets.viewstackpage import ViewStackPage from melon.widgets.iconbutton import IconButton @@ -18,10 +19,10 @@ class Subscriptions(ViewStackPage): subs = filter_resources(get_subscribed_channels(), app_settings) if len(subs) == 0: status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("You aren't yet subscribed to channels") + status.set_title(_("*crickets chirping*")) + status.set_description(_("You aren't yet subscribed to channels")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Browse Servers", "list-add-symbolic") + icon_button = IconButton(_("Browse Servers"), "list-add-symbolic") icon_button.set_action_name("win.browse") box = Gtk.CenterBox() box.set_center_widget(icon_button) @@ -29,8 +30,8 @@ class Subscriptions(ViewStackPage): self.inner.set_child(status) else: results = Adw.PreferencesGroup() - results.set_title("Subscriptions") - results.set_description("You are subscribed to the following channels") + results.set_title(_("Subscriptions")) + results.set_description(_("You are subscribed to the following channels")) subs.sort(key=lambda c:c.name) # append first, so subscriptions are added as we go # not all at once @@ -64,7 +65,7 @@ class Subscriptions(ViewStackPage): self.thread.start() def __init__(self): - super().__init__("subs", "Subscriptions", "x-office-address-book-symbolic") + super().__init__("subs", _("Subscriptions"), "x-office-address-book-symbolic") self.widget = Adw.Clamp() self.inner = Gtk.ScrolledWindow() self.widget.set_child(self.inner) diff --git a/melon/import_providers/newpipe.py b/melon/import_providers/newpipe.py index 38b9a10..a1e0e76 100644 --- a/melon/import_providers/newpipe.py +++ b/melon/import_providers/newpipe.py @@ -1,6 +1,7 @@ from abc import ABC import sqlite3 import json +from gettext import gettext as _ from melon.import_providers import ImportProvider, PickerMode from melon.servers import Channel, Playlist, Video @@ -24,9 +25,9 @@ sqlite3.register_converter('json', convert_json) class NewpipeImporter(ImportProvider, ABC): id = "newpipedb" - title = "Newpipe Database importer" - description = "Import the .db file from inside the newpipe .zip export (as invidious content)" - picker_title = "Select .db file" + title = _("Newpipe Database importer") + description = _("Import the .db file from inside the newpipe .zip export (as invidious content)") + picker_title = _("Select .db file") # the server id for resources server_id:str @@ -41,7 +42,7 @@ class NewpipeImporter(ImportProvider, ABC): is_multi = False filters = { - "Newpipe Database": [ "application/x-sqlite3" ] + _("Newpipe Database"): [ "application/x-sqlite3" ] } db:sqlite3.Connection = None diff --git a/melon/importer.py b/melon/importer.py index 752c14a..7cf3117 100644 --- a/melon/importer.py +++ b/melon/importer.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio +from gettext import gettext as _ from melon.import_providers.utils import ImportPicker from melon.import_providers import ImportProvider, PickerMode @@ -13,7 +14,7 @@ from melon.models import get_app_settings class ImporterScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("Import") + self.set_title(_("Import")) self.header_bar = Adw.HeaderBar() @@ -31,8 +32,8 @@ class ImporterScreen(Adw.NavigationPage): def reset(self): servers = get_allowed_servers_list(get_app_settings()) self.box = Adw.PreferencesPage() - self.box.set_title("Import") - self.box.set_description("The following import methods have been found") + self.box.set_title(_("Import")) + self.box.set_description(_("The following import methods have been found")) count = 0 for server in servers: @@ -57,10 +58,10 @@ class ImporterScreen(Adw.NavigationPage): if count == 0: # if no servers are allowed, show the nothing here box status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("There are no available importers") + status.set_title(_("*crickets chirping*")) + status.set_description(_("There are no available importer methods")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Enable servers in the settings menu", "list-add-symbolic") + icon_button = IconButton(_("Enable servers in the settings menu"), "list-add-symbolic") icon_button.set_action_name("win.prefs") box = Gtk.CenterBox() box.set_center_widget(icon_button) diff --git a/melon/player/__init__.py b/melon/player/__init__.py index ce4cb75..0259464 100644 --- a/melon/player/__init__.py +++ b/melon/player/__init__.py @@ -3,6 +3,8 @@ gi.require_version("WebKit", "6.0") gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, WebKit, GLib +from unidecode import unidecode +from gettext import gettext as _ from melon.servers.utils import get_server_instance, get_servers_list from melon.servers import SearchMode @@ -10,7 +12,6 @@ from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.preferencerow import PreferenceRow, PreferenceType, Preference from melon.widgets.iconbutton import IconButton from melon.models import get_app_settings, notify, add_to_history -from unidecode import unidecode class PlayerScreen(Adw.NavigationPage): def on_open_in_browser(self, arg): @@ -61,7 +62,8 @@ class PlayerScreen(Adw.NavigationPage): # stream selector pref = Preference( "quality", - "Quality", "Video quality", + _("Quality"), + _("Video quality"), PreferenceType.DROPDOWN, [ unidecode(stream.quality) for stream in self.streams ], default_stream) @@ -71,7 +73,7 @@ class PlayerScreen(Adw.NavigationPage): # expandable description field desc_field = Adw.ExpanderRow() - desc_field.set_title("Description") + desc_field.set_title(_("Description")) desc_field.set_subtitle(unidecode(self.video.description[:40]).replace("&","&")+"...") desc = Adw.ActionRow() desc.set_subtitle(unidecode(self.video.description).replace("&","&")) @@ -86,8 +88,8 @@ class PlayerScreen(Adw.NavigationPage): # add to playlist button btn_bookmark = Adw.ActionRow() - btn_bookmark.set_title("Bookmark") - btn_bookmark.set_subtitle("Add this video to a playlist") + btn_bookmark.set_title(_("Bookmark")) + btn_bookmark.set_subtitle(_("Add this video to a playlist")) btn_bookmark.add_suffix(Gtk.Image.new_from_icon_name("user-bookmarks-symbolic")) btn_bookmark.set_activatable(True) btn_bookmark.set_action_name("win.add_to_playlist") diff --git a/melon/playlist/__init__.py b/melon/playlist/__init__.py index c099793..721bcf2 100644 --- a/melon/playlist/__init__.py +++ b/melon/playlist/__init__.py @@ -3,13 +3,15 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw +from unidecode import unidecode +from gettext import gettext as _ + from melon.servers.utils import get_servers_list, get_server_instance from melon.servers import Preference, PreferenceType from melon.widgets.iconbutton import IconButton from melon.widgets.feeditem import AdaptiveFeedItem from melon.models import get_app_settings, get_local_playlist from melon.models import is_server_enabled, ensure_server_disabled, ensure_server_enabled -from unidecode import unidecode class LocalPlaylistScreen(Adw.NavigationPage): def __init__(self, playlist_id, *args, **kwargs): @@ -30,10 +32,10 @@ class LocalPlaylistScreen(Adw.NavigationPage): if len(playlist.content) == 0: status = Adw.StatusPage() - status.set_title("*crickets chirping*") - status.set_description("You haven't added any videos to this playlist yet") + status.set_title(_("*crickets chirping*")) + status.set_description(_("You haven't added any videos to this playlist yet")) status.set_icon_name("weather-few-clouds-night-symbolic") - icon_button = IconButton("Start watching", "video-display-symbolic") + icon_button = IconButton(_("Start watching"), "video-display-symbolic") icon_button.set_action_name("win.home") box = Gtk.CenterBox() box.set_center_widget(icon_button) diff --git a/melon/playlist/create.py b/melon/playlist/create.py index d8b7131..d3ebd1a 100644 --- a/melon/playlist/create.py +++ b/melon/playlist/create.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, GLib +from gettext import gettext as _ from melon.widgets.iconbutton import IconButton from melon.widgets.simpledialog import SimpleDialog @@ -13,15 +14,15 @@ from melon.servers.utils import get_app_settings class PlaylistCreatorDialog(SimpleDialog): def __init__(self, video=None, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("New Playlist") + self.set_title(_("New Playlist")) page = Adw.PreferencesPage() self.content = [] if not video is None: self.content = [ video ] # use preference group as preview for video element preview_group = Adw.PreferencesGroup() - preview_group.set_title("Video") - preview_group.set_description("The following video will be added to the new playlist") + preview_group.set_title(_("Video")) + preview_group.set_description(_("The following video will be added to the new playlist")) preview_group.add(AdaptiveFeedItem( video, clickable=False, @@ -30,22 +31,22 @@ class PlaylistCreatorDialog(SimpleDialog): page.add(preview_group) # use preference group for input input_group = Adw.PreferencesGroup() - input_group.set_title("New Playlist") - input_group.set_description("Enter more playlist information") + input_group.set_title(_("New Playlist")) + input_group.set_description(_("Enter more playlist information")) self.input_title = Adw.EntryRow() - self.input_title.set_title("Playlist Name") - self.input_title.set_text("Unnamed Playlist") + self.input_title.set_title(_("Playlist Name")) + self.input_title.set_text(_("Unnamed Playlist")) self.input_desc = Adw.EntryRow() - self.input_desc.set_title("Playlist description") + self.input_desc.set_title(_("Playlist description")) input_group.add(self.input_title) input_group.add(self.input_desc) page.add(input_group) bottom_bar = Gtk.Box() - btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create playlist") - btn_confirm = IconButton("Create", "list-add-symbolic", tooltip="Create playlist") + btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist")) + btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip=_("Create playlist")) btn_confirm.connect( "clicked", self.create_playlist diff --git a/melon/playlist/pick.py b/melon/playlist/pick.py index 687521f..fedc916 100644 --- a/melon/playlist/pick.py +++ b/melon/playlist/pick.py @@ -4,6 +4,7 @@ gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, GLib, Gdk from unidecode import unidecode +from gettext import gettext as _ from melon.widgets.feeditem import AdaptiveFeedItem from melon.widgets.iconbutton import IconButton @@ -19,13 +20,13 @@ class PlaylistPickerDialog(SimpleDialog): diag.show() def __init__(self, video, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("Add to Playlist") + self.set_title(_("Add to Playlist")) page = Adw.PreferencesPage() # use preference group as preview for video element preview_group = Adw.PreferencesGroup() - preview_group.set_title("Video") - preview_group.set_description("The following video will be added to the playlist") + preview_group.set_title(_("Video")) + preview_group.set_description(_("The following video will be added to the playlist")) preview_group.add(AdaptiveFeedItem( video, clickable=False, @@ -35,11 +36,11 @@ class PlaylistPickerDialog(SimpleDialog): group = Adw.PreferencesGroup() page.add(group) - group.set_title("Add to playlist") - group.set_description("Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones") + group.set_title(_("Add to playlist")) + group.set_description(_("Choose a playlist to add the video to. Note that you can only add videos to local playlists, not external bookmarked ones")) make_new = Adw.ActionRow() - make_new.set_title("Create new playlist") - make_new.set_subtitle("Create a new playlist and add the video to it") + make_new.set_title(_("Create new playlist")) + make_new.set_subtitle(_("Create a new playlist and add the video to it")) make_new.add_suffix(Gtk.Image.new_from_icon_name("go-next-symbolic")) # I can't make this work ): # make_new.set_action_name("app.add_to_new_playlist") @@ -82,7 +83,7 @@ class PlaylistPickerDialog(SimpleDialog): group.add(row) bottom_bar = Gtk.Box() - btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create playlist") + btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not create playlist")) padding = 12 btn_cancel.set_vexpand(True) btn_cancel.set_hexpand(True) diff --git a/melon/servers/invidious/__init__.py b/melon/servers/invidious/__init__.py index 6e82b39..f530d4f 100644 --- a/melon/servers/invidious/__init__.py +++ b/melon/servers/invidious/__init__.py @@ -1,11 +1,13 @@ -from melon.servers import Server, Preference, PreferenceType -from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode -from melon.servers import USER_AGENT from bs4 import BeautifulSoup import requests from urllib.parse import urlparse,parse_qs from datetime import datetime +from gettext import gettext as _ + from melon.import_providers.newpipe import NewpipeImporter +from melon.servers import Server, Preference, PreferenceType +from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode +from melon.servers import USER_AGENT class NewpipeInvidiousImporter(NewpipeImporter): server_id = "invidious" @@ -22,13 +24,13 @@ class Invidious(Server): id = "invidious" name = "Invidious" - description = "Open source alternative front-end to YouTube" + description = _("Open source alternative front-end to YouTube") settings = { "instance": Preference( "instance", - "Instance", - "See https://docs.invidious.io/instances/ for a list of available instances", + _("Instance"), + _("See https://docs.invidious.io/instances/ for a list of available instances"), PreferenceType.TEXT, "https://inv.tux.pizza", "https://inv.tux.pizza"), @@ -39,8 +41,8 @@ class Invidious(Server): is_nsfw = True, known_public_feeds = { - "/feed/trending": Feed("trending", "Trending", "starred-symbolic"), - "/feed/popular": Feed("popular", "Popular", "emblem-favorite-symbolic") + "/feed/trending": Feed("trending", _("Trending"), "starred-symbolic"), + "/feed/popular": Feed("popular", _("Popular"), "emblem-favorite-symbolic") } def get_external_url(self): diff --git a/melon/servers/nebula/__init__.py b/melon/servers/nebula/__init__.py index 9c1af68..1219a88 100644 --- a/melon/servers/nebula/__init__.py +++ b/melon/servers/nebula/__init__.py @@ -1,11 +1,13 @@ -from melon.servers import Server, Preference, PreferenceType -from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode -from melon.servers import USER_AGENT from bs4 import BeautifulSoup import requests -from urllib.parse import urlparse,parse_qs +from urllib.parse import urlparse, parse_qs from datetime import datetime import json +from gettext import gettext as _ + +from melon.servers import Server, Preference, PreferenceType +from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode +from melon.servers import USER_AGENT # NOTE: uses beautifulsoup instead of the invidious api # because not all invidious servers provide the api @@ -14,26 +16,26 @@ class Nebula(Server): id = "nebula" name = "Nebula" - description = "Home of smart, thoughtful videos, podcasts, and classes from your favorite creators" + description = _("Home of smart, thoughtful videos, podcasts, and classes from your favorite creators") settings = { "email": Preference( "email", - "Email Address", - "Email Address to login to your account", + _("Email Address"), + _("Email Address to login to your account"), PreferenceType.TEXT, "","" ), "password": Preference( "password", - "Password", - "Password associated with your account", + _("Password"), + _("Password associated with your account"), PreferenceType.PASSWORD, "",""), "m3u8-player": Preference( "m3u8-player", - ".m3u8 Player", - "valid .m3u8 web-pased player url. %s will be replaced with playlist url", + _(".m3u8 Player"), + _("valid .m3u8 web-pased player url. %s will be replaced with playlist url"), PreferenceType.TEXT, "https://www.hlsplayer.org/play?url=%s", "https://www.hlsplayer.org/play?url=%s") @@ -65,19 +67,19 @@ class Nebula(Server): if listing["title"] == "Latest Videos": feeds.append(Feed( listing["id"], - listing["title"], + _(listing["title"]), icon="dialog-information-symbolic") .with_url(listing["view_all_url"])) elif listing["title"] == "Nebula Originals": feeds.append(Feed( listing["id"], - listing["title"], + _(listing["title"]), icon="starred-symbolic" ).with_url(listing["view_all_url"])) elif listing["title"] == "Nebula Plus": feeds.append(Feed( listing["id"], - listing["title"], + _(listing["title"]), icon="list-add-symbolic" ).with_url(listing["view_all_url"])) return feeds @@ -189,13 +191,13 @@ class Nebula(Server): def get_channel_feeds(self, channel_id:str): url = f"https://content.api.nebula.app/video_channels/{channel_id}/" r = requests.get(url, headers={ "User-Agent": USER_AGENT }) - video_feed = Feed("videos", "Videos") + video_feed = Feed("videos", _("Videos")) if r.ok: data = json.loads(r.text) if (not "playlist" in data) or len(data["playlist"]) == 0: return [ video_feed ] else: - return [ video_feed, Feed("playlists", "Playlists") ] + return [ video_feed, Feed("playlists", _("Playlists")) ] def get_default_channel_feed(self, cid:str): return "videos" diff --git a/melon/servers/peertube/__init__.py b/melon/servers/peertube/__init__.py index 1f5d792..c02e82e 100644 --- a/melon/servers/peertube/__init__.py +++ b/melon/servers/peertube/__init__.py @@ -1,35 +1,37 @@ -from melon.servers import Server, Preference, PreferenceType -from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode -from melon.servers import USER_AGENT import requests from urllib.parse import urlparse,parse_qs from datetime import datetime +from gettext import gettext as _ + +from melon.servers import Server, Preference, PreferenceType +from melon.servers import Feed, Channel, Video, Playlist, Stream, SearchMode +from melon.servers import USER_AGENT from melon.import_providers.newpipe import NewpipeImporter class Peertube(Server): id = "peertube" name = "Peertube" - description = "Decentralized video hosting network, based on free/libre software" + description = _("Decentralized video hosting network, based on free/libre software") settings = { "instances": Preference( "instances", - "Instances", - "List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances", + _("Instances"), + _("List of peertube instances, from which to fetch content. See https://joinpeertube.org/instances"), PreferenceType.MULTI, ["https://tilvids.com/"], ["https://tilvids.com/"]), "nsfw": Preference( "nsfw", - "Show NSFW content", - "Passes the nsfw filter to the peertube search API", + _("Show NSFW content"), + _("Passes the nsfw filter to the peertube search API"), PreferenceType.TOGGLE, False, False), "federate": Preference( "federate", - "Enable Federation", - "Returns content from federated instances instead of only local content", + _("Enable Federation"), + _("Returns content from federated instances instead of only local content"), PreferenceType.TOGGLE, True, True) } @@ -53,7 +55,7 @@ class Peertube(Server): # nor is it possible to show trending videos, # because they cannot be merged return [ - Feed("latest", "Latest", "dialog-information-symbolic") + Feed("latest", _("Latest"), "dialog-information-symbolic") ] def get_query_config(self) -> str: @@ -199,11 +201,11 @@ class Peertube(Server): res = self.get_channel_feed_content(cid, "playlists") if len(res) != 0: return [ - Feed("videos", "Videos"), - Feed("playlists", "Playlists") + Feed("videos", _("Videos")), + Feed("playlists", _("Playlists")) ] return [ - Feed("videos", "Videos") + Feed("videos", _("Videos")) ] def get_default_channel_feed(self, cid:str): diff --git a/melon/servers/utils.py b/melon/servers/utils.py index 746789b..55f25c3 100644 --- a/melon/servers/utils.py +++ b/melon/servers/utils.py @@ -213,8 +213,6 @@ def get_servers_list(include_disabled=False, order_by='name'): modules.append(importlib.import_module(relname, package='melon.servers')) count += 1 - - logger.info('Load {0} servers from external folder: {1}'.format(count, servers_path)) else: # fallback to local exploration import melon.servers diff --git a/melon/settings/__init__.py b/melon/settings/__init__.py index bf9b28d..a999162 100644 --- a/melon/settings/__init__.py +++ b/melon/settings/__init__.py @@ -3,6 +3,8 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw +from gettext import gettext as _ + from melon.servers.utils import get_servers_list, get_server_instance from melon.servers import Preference, PreferenceType from melon.widgets.preferencerow import PreferenceRow @@ -13,35 +15,35 @@ global_prefs = { # True if images should be shown when searching or browsing public feeds "show_images_in_browse": Preference( "show_images_in_browse", - "Show Previews when browsing public feeds (incl. search)", - "Set to true to show previews when viewing channel contents, public feeds and searching", + _("Show Previews when browsing public feeds (incl. search)"), + _("Set to true to show previews when viewing channel contents, public feeds and searching"), PreferenceType.TOGGLE, True, True), # True if images should be shown when browsing channels/playlists/home feed "show_images_in_feed": Preference( "show_images_in_feed", - "Show Previews in local feeds", - "Set to true to show previews in the new feed, for subscribed channels, and saved playlists", + _("Show Previews in local feeds"), + _("Set to true to show previews in the new feed, for subscribed channels, and saved playlists"), PreferenceType.TOGGLE, True, True), "nsfw_content": Preference( "nsfw_content", - "Show servers that may contain nsfw content", - "Lists/Delists servers in the browse servers list, if they contain some nsfw content", + _("Show servers that may contain nsfw content"), + _("Lists/Delists servers in the browse servers list, if they contain some nsfw content"), PreferenceType.TOGGLE, True, True), "nsfw_only_content": Preference( "nsfw_only_content", - "Show servers that only contain nsfw content", - "Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content", + _("Show servers that only contain nsfw content"), + _("Lists/Delists servers in the browse servers list, if they contain only/mostly nsfw content"), PreferenceType.TOGGLE, True, True ), "login_required": Preference( "login_required", - "Show servers that require login", - "Lists/Delists servers in the browse servers list, if they require login to function", + _("Show servers that require login"), + _("Lists/Delists servers in the browse servers list, if they require login to function"), PreferenceType.TOGGLE, True, True ) @@ -50,7 +52,7 @@ global_prefs = { class SettingsScreen(Adw.NavigationPage): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_title("Settings") + self.set_title(_("Settings")) self.header_bar = Adw.HeaderBar() @@ -63,8 +65,8 @@ class SettingsScreen(Adw.NavigationPage): # global app settings self.general = Adw.PreferencesGroup() - self.general.set_title("General") - self.general.set_description("Global app settings") + self.general.set_title(_("General")) + self.general.set_description(_("Global app settings")) app_conf = get_app_settings() for pref_id, pref in global_prefs.items(): # manually fetch current values @@ -88,8 +90,8 @@ class SettingsScreen(Adw.NavigationPage): instance = get_server_instance(server) epref = Preference( "server-enabled", - "Enable Server", - "Disabled servers won't show up in the browser or on the local/home screen", + _("Enable Server"), + _("Disabled servers won't show up in the browser or on the local/home screen"), PreferenceType.TOGGLE, True, is_server_enabled(server_id)) er = PreferenceRow(epref) diff --git a/melon/widgets/preferencerow.py b/melon/widgets/preferencerow.py index 090e866..949ca97 100644 --- a/melon/widgets/preferencerow.py +++ b/melon/widgets/preferencerow.py @@ -3,11 +3,14 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, GLib, Gdk + +from gettext import gettext as _ +from copy import deepcopy as copy + from melon.servers import Resource,Video,Playlist,Channel,Preference,PreferenceType from melon.servers.utils import pixbuf_from_url from melon.widgets.iconbutton import IconButton from melon.widgets.simpledialog import SimpleDialog -from copy import deepcopy as copy class PreferenceRow(): def __init__(self, pref: Preference, *args, **kwargs): @@ -109,7 +112,7 @@ class MultiRow(Adw.PreferencesRow): self.set_child(self.inner) self.inner.set_title(self.pref.name) self.inner.set_description(self.pref.description) - self.adder = IconButton("Add", "list-add-symbolic") + self.adder = IconButton(_("Add"), "list-add-symbolic") self.adder.connect("clicked", lambda x: self.open_add()) self.inner.set_header_suffix(self.adder) counter = 0 @@ -123,7 +126,7 @@ class MultiRow(Adw.PreferencesRow): # not shown for first element if counter > 0: move_up = IconButton("","go-up-symbolic") - move_up.set_tooltip_text("Move up") + move_up.set_tooltip_text(_("Move up")) move_up.add_css_class("circular") move_up.connect( "clicked", @@ -134,7 +137,7 @@ class MultiRow(Adw.PreferencesRow): # not shown for last element if counter < length-1: move_down = IconButton("", "go-down-symbolic") - move_down.set_tooltip_text("Move down") + move_down.set_tooltip_text(_("Move down")) move_down.add_css_class("circular") move_down.connect( "clicked", @@ -143,7 +146,7 @@ class MultiRow(Adw.PreferencesRow): actions.append(move_down) # remove button remove = IconButton("", "list-remove-symbolic") - remove.set_tooltip_text("Remove from list") + remove.set_tooltip_text(_("Remove from list")) remove.add_css_class("circular") remove.connect("clicked", pass_me(self.confirm_delete, item, counter)) actions.append(remove) @@ -156,20 +159,20 @@ class MultiRow(Adw.PreferencesRow): self.update() def open_add(self): diag = SimpleDialog() - diag.set_title("Add Item") + diag.set_title(_("Add Item")) # preview prferences group box = Adw.PreferencesGroup(); - box.set_title("Create a new list entry") - box.set_description("Enter the new value here") + box.set_title(_("Create a new list entry")) + box.set_description(_("Enter the new value here")) # text input inp = Adw.EntryRow() - inp.set_title("Value") + inp.set_title(_("Value")) box.add(inp) diag.set_widget(box) # place button bar in toolbar bottom_bar = Gtk.Box() - btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not create a new item") - btn_confirm = IconButton("Create", "list-add-symbolic", tooltip="Add entry to list") + btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip="Do not create a new item") + btn_confirm = IconButton(_("Create"), "list-add-symbolic", tooltip="Add entry to list") btn_confirm.connect( "clicked", lambda _: many( @@ -200,19 +203,19 @@ class MultiRow(Adw.PreferencesRow): diag.show() def confirm_delete(self, _, item:str, counter:int): diag = SimpleDialog() - diag.set_title("Delete") + diag.set_title(_("Delete")) # preview prferences group preview = Adw.PreferencesGroup(); - preview.set_title("Do you really want to delete this item?") - preview.set_description("You won't be able to restore it afterwards") + preview.set_title(_("Do you really want to delete this item?")) + preview.set_description(_("You won't be able to restore it afterwards")) row = Adw.ActionRow() row.set_title(item) preview.add(row) diag.set_widget(preview) # place button bar in toolbar bottom_bar = Gtk.Box() - btn_cancel = IconButton("Cancel", "process-stop-symbolic", tooltip="Do not remove item") - btn_confirm = IconButton("Delete", "list-add-symbolic", tooltip="Remove item from list") + btn_cancel = IconButton(_("Cancel"), "process-stop-symbolic", tooltip=_("Do not remove item")) + btn_confirm = IconButton(_("Delete"), "list-add-symbolic", tooltip=_("Remove item from list")) btn_confirm.connect( "clicked", lambda _: many( diff --git a/melon/window.py b/melon/window.py index 81e4817..6c29543 100644 --- a/melon/window.py +++ b/melon/window.py @@ -3,6 +3,7 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') from gi.repository import Gtk, Adw, Gio, GLib +from gettext import gettext as _ from melon.home import HomeScreen from melon.browse import BrowseScreen @@ -77,7 +78,7 @@ class MainWindow(Adw.ApplicationWindow): dialog.set_version("0.1.2") dialog.set_developer_name("Jakob Meier (@comcloudway)") dialog.set_license_type(Gtk.License(Gtk.License.GPL_3_0)) - dialog.set_comments("Stream videos on the go") + dialog.set_comments(_("Stream videos on the go")) dialog.set_website("https://codeberg.org/comcloudway/melon") dialog.set_issue_url("https://codeberg.org/comcloudway/melon/issues") #dialog.add_credit_section("Contributors", ["Name1 url"]) @@ -85,7 +86,7 @@ class MainWindow(Adw.ApplicationWindow): dialog.set_copyright("© 2024 Jakob Meier (@comcloudway)") dialog.set_developers(["Jakob Meier (@comcloudway)"]) # TODO: icon must be uploaded in ~/.local/share/icons or /usr/share/icons - dialog.set_application_icon("icu.ccw.melon") + dialog.set_application_icon("icu.ccw.Melon") dialog.set_visible(True) # opens the setting panel def open_settings(self,action,prefs): diff --git a/meson.build b/meson.build index 13796a6..3a5031c 100644 --- a/meson.build +++ b/meson.build @@ -37,8 +37,10 @@ app_id_aspath = '/'.join([domainext, domainname, prettyname]) app_id = base_id install_subdir(meson.project_name(), install_dir: pythondir) + subdir('data') subdir('bin') +subdir('po') # Run required post-install steps gnome.post_install( diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..e5356f3 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1 @@ +# Please keep this list alphabetically sorted diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..1af9812 --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,22 @@ +data/icu.ccw.Melon.desktop.in +melon/home/history.py +melon/home/new.py +melon/home/playlists.py +melon/home/subs.py +melon/settings/__init__.py +melon/servers/invidious/__init__.py +melon/servers/nebula/__init__.py +melon/servers/peertube/__init__.py +melon/widgets/preferencerow.py +melon/browse/channel.py +melon/browse/playlist.py +melon/browse/__init__.py +melon/browse/search.py +melon/browse/server.py +melon/player/__init__.py +melon/playlist/create.py +melon/playlist/__init__.py +melon/playlist/pick.py +melon/import_providers/newpipe.py +melon/importer.py +melon/window.py diff --git a/po/melon.pot b/po/melon.pot new file mode 100644 index 0000000..f816096 --- /dev/null +++ b/po/melon.pot @@ -0,0 +1,536 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the Melon package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Melon 0.1.2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-01 12:20+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: ../melon/home/history.py:27 ../melon/home/new.py:27 +#: ../melon/home/playlists.py:25 ../melon/home/subs.py:22 +#: ../melon/browse/__init__.py:18 ../melon/browse/search.py:58 +#: ../melon/browse/server.py:101 ../melon/browse/server.py:125 +#: ../melon/playlist/__init__.py:35 ../melon/importer.py:61 +msgid "*crickets chirping*" +msgstr "" + +#: ../melon/home/history.py:28 +msgid "You haven't watched any videos yet" +msgstr "" + +#: ../melon/home/history.py:30 ../melon/home/new.py:34 ../melon/home/subs.py:25 +msgid "Browse Servers" +msgstr "" + +#: ../melon/home/history.py:39 ../melon/home/history.py:82 +msgid "History" +msgstr "" + +#: ../melon/home/history.py:40 +msgid "These are the videos you opened in the past" +msgstr "" + +#: ../melon/home/new.py:21 +msgid "Refresh" +msgstr "" + +#: ../melon/home/new.py:30 +msgid "Subscribe to a channel first, to view new uploads" +msgstr "" + +#: ../melon/home/new.py:32 +msgid "The channels you are subscribed to haven't uploaded anything yet" +msgstr "" + +#: ../melon/home/new.py:53 +#, python-brace-format +msgid "(Last refresh: {last_refresh})" +msgstr "" + +#: ../melon/home/new.py:55 ../melon/home/new.py:108 +msgid "What's new" +msgstr "" + +#: ../melon/home/new.py:56 +msgid "These are the latest videos of channels you follow" +msgstr "" + +#: ../melon/home/playlists.py:26 +msgid "You don't have any playlists yet" +msgstr "" + +#: ../melon/home/playlists.py:28 ../melon/home/playlists.py:38 +msgid "Create a new playlist" +msgstr "" + +#: ../melon/home/playlists.py:36 ../melon/home/playlists.py:69 +#: ../melon/servers/nebula/__init__.py:200 +#: ../melon/servers/peertube/__init__.py:205 ../melon/browse/search.py:88 +#: ../melon/browse/server.py:48 +msgid "Playlists" +msgstr "" + +#: ../melon/home/playlists.py:37 +msgid "Here are playlists you've bookmarked or created yourself" +msgstr "" + +#: ../melon/home/playlists.py:38 +msgid "New" +msgstr "" + +#: ../melon/home/subs.py:23 +msgid "You aren't yet subscribed to channels" +msgstr "" + +#: ../melon/home/subs.py:33 ../melon/home/subs.py:68 +msgid "Subscriptions" +msgstr "" + +#: ../melon/home/subs.py:34 +msgid "You are subscribed to the following channels" +msgstr "" + +#: ../melon/settings/__init__.py:18 +msgid "Show Previews when browsing public feeds (incl. search)" +msgstr "" + +#: ../melon/settings/__init__.py:19 +msgid "" +"Set to true to show previews when viewing channel contents, public feeds and " +"searching" +msgstr "" + +#: ../melon/settings/__init__.py:25 +msgid "Show Previews in local feeds" +msgstr "" + +#: ../melon/settings/__init__.py:26 +msgid "" +"Set to true to show previews in the new feed, for subscribed channels, and " +"saved playlists" +msgstr "" + +#: ../melon/settings/__init__.py:32 +msgid "Show servers that may contain nsfw content" +msgstr "" + +#: ../melon/settings/__init__.py:33 +msgid "" +"Lists/Delists servers in the browse servers list, if they contain some nsfw " +"content" +msgstr "" + +#: ../melon/settings/__init__.py:38 +msgid "Show servers that only contain nsfw content" +msgstr "" + +#: ../melon/settings/__init__.py:39 +msgid "" +"Lists/Delists servers in the browse servers list, if they contain only/" +"mostly nsfw content" +msgstr "" + +#: ../melon/settings/__init__.py:45 +msgid "Show servers that require login" +msgstr "" + +#: ../melon/settings/__init__.py:46 +msgid "" +"Lists/Delists servers in the browse servers list, if they require login to " +"function" +msgstr "" + +#: ../melon/settings/__init__.py:55 +msgid "Settings" +msgstr "" + +#: ../melon/settings/__init__.py:68 +msgid "General" +msgstr "" + +#: ../melon/settings/__init__.py:69 +msgid "Global app settings" +msgstr "" + +#: ../melon/settings/__init__.py:93 +msgid "Enable Server" +msgstr "" + +#: ../melon/settings/__init__.py:94 +msgid "" +"Disabled servers won't show up in the browser or on the local/home screen" +msgstr "" + +#: ../melon/servers/invidious/__init__.py:27 +msgid "Open source alternative front-end to YouTube" +msgstr "" + +#: ../melon/servers/invidious/__init__.py:32 +msgid "Instance" +msgstr "" + +#: ../melon/servers/invidious/__init__.py:33 +msgid "" +"See https://docs.invidious.io/instances/ for a list of available instances" +msgstr "" + +#: ../melon/servers/invidious/__init__.py:44 +msgid "Trending" +msgstr "" + +#: ../melon/servers/invidious/__init__.py:45 +msgid "Popular" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:19 +msgid "" +"Home of smart, thoughtful videos, podcasts, and classes from your favorite " +"creators" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:24 +msgid "Email Address" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:25 +msgid "Email Address to login to your account" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:31 +msgid "Password" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:32 +msgid "Password associated with your account" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:37 +msgid ".m3u8 Player" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:38 +#, python-format +msgid "valid .m3u8 web-pased player url. %s will be replaced with playlist url" +msgstr "" + +#: ../melon/servers/nebula/__init__.py:194 +#: ../melon/servers/peertube/__init__.py:204 +#: ../melon/servers/peertube/__init__.py:208 ../melon/browse/search.py:91 +#: ../melon/browse/server.py:51 +msgid "Videos" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:15 +msgid "Decentralized video hosting network, based on free/libre software" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:20 +msgid "Instances" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:21 +msgid "" +"List of peertube instances, from which to fetch content. See https://" +"joinpeertube.org/instances" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:27 +msgid "Show NSFW content" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:28 +msgid "Passes the nsfw filter to the peertube search API" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:33 +msgid "Enable Federation" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:34 +msgid "Returns content from federated instances instead of only local content" +msgstr "" + +#: ../melon/servers/peertube/__init__.py:58 +msgid "Latest" +msgstr "" + +#: ../melon/widgets/preferencerow.py:115 +msgid "Add" +msgstr "" + +#: ../melon/widgets/preferencerow.py:129 +msgid "Move up" +msgstr "" + +#: ../melon/widgets/preferencerow.py:140 +msgid "Move down" +msgstr "" + +#: ../melon/widgets/preferencerow.py:149 +msgid "Remove from list" +msgstr "" + +#: ../melon/widgets/preferencerow.py:162 +msgid "Add Item" +msgstr "" + +#: ../melon/widgets/preferencerow.py:165 +msgid "Create a new list entry" +msgstr "" + +#: ../melon/widgets/preferencerow.py:166 +msgid "Enter the new value here" +msgstr "" + +#: ../melon/widgets/preferencerow.py:169 +msgid "Value" +msgstr "" + +#: ../melon/widgets/preferencerow.py:174 ../melon/widgets/preferencerow.py:217 +#: ../melon/playlist/create.py:48 ../melon/playlist/pick.py:86 +msgid "Cancel" +msgstr "" + +#: ../melon/widgets/preferencerow.py:175 ../melon/playlist/create.py:49 +msgid "Create" +msgstr "" + +#: ../melon/widgets/preferencerow.py:206 ../melon/widgets/preferencerow.py:218 +msgid "Delete" +msgstr "" + +#: ../melon/widgets/preferencerow.py:209 +msgid "Do you really want to delete this item?" +msgstr "" + +#: ../melon/widgets/preferencerow.py:210 +msgid "You won't be able to restore it afterwards" +msgstr "" + +#: ../melon/widgets/preferencerow.py:217 +msgid "Do not remove item" +msgstr "" + +#: ../melon/widgets/preferencerow.py:218 +msgid "Remove item from list" +msgstr "" + +#: ../melon/browse/channel.py:99 +msgid "Subscribe to channel" +msgstr "" + +#: ../melon/browse/channel.py:100 +msgid "Add latest uploads to home feed" +msgstr "" + +#: ../melon/browse/channel.py:127 +msgid "Channel feed" +msgstr "" + +#: ../melon/browse/channel.py:128 +msgid "This channel provides multiple feeds, choose which one to view" +msgstr "" + +#: ../melon/browse/playlist.py:61 ../melon/player/__init__.py:91 +msgid "Bookmark" +msgstr "" + +#: ../melon/browse/playlist.py:62 +msgid "Add Playlist to your local playlist collection" +msgstr "" + +#: ../melon/browse/__init__.py:19 +msgid "There are no available servers" +msgstr "" + +#: ../melon/browse/__init__.py:21 ../melon/browse/search.py:61 +#: ../melon/importer.py:64 +msgid "Enable servers in the settings menu" +msgstr "" + +#: ../melon/browse/__init__.py:29 +msgid "Available Servers" +msgstr "" + +#: ../melon/browse/__init__.py:30 +msgid "You can enable/disable and filter servers in the settings menu" +msgstr "" + +#: ../melon/browse/__init__.py:48 +msgid "Servers" +msgstr "" + +#: ../melon/browse/__init__.py:56 ../melon/browse/search.py:106 +msgid "Global Search" +msgstr "" + +#: ../melon/browse/search.py:44 +msgid "No results" +msgstr "" + +#: ../melon/browse/search.py:46 +#, python-brace-format +msgid "{count} result" +msgid_plural "{count} results" +msgstr[0] "" +msgstr[1] "" + +#: ../melon/browse/search.py:59 +msgid "There are no available servers, a search would yield no results" +msgstr "" + +#: ../melon/browse/search.py:83 ../melon/browse/server.py:43 +msgid "Any" +msgstr "" + +#: ../melon/browse/search.py:85 ../melon/browse/server.py:45 +msgid "Channels" +msgstr "" + +#: ../melon/browse/server.py:23 +msgid "Search" +msgstr "" + +#: ../melon/browse/server.py:102 +msgid "Try searching for a term" +msgstr "" + +#: ../melon/browse/server.py:104 +msgid "Try using a different query" +msgstr "" + +#: ../melon/browse/server.py:126 +msgid "This feed is empty" +msgstr "" + +#: ../melon/player/__init__.py:65 +msgid "Quality" +msgstr "" + +#: ../melon/player/__init__.py:66 +msgid "Video quality" +msgstr "" + +#: ../melon/player/__init__.py:76 +msgid "Description" +msgstr "" + +#: ../melon/player/__init__.py:92 +msgid "Add this video to a playlist" +msgstr "" + +#: ../melon/playlist/create.py:17 ../melon/playlist/create.py:34 +msgid "New Playlist" +msgstr "" + +#: ../melon/playlist/create.py:24 ../melon/playlist/pick.py:28 +msgid "Video" +msgstr "" + +#: ../melon/playlist/create.py:25 +msgid "The following video will be added to the new playlist" +msgstr "" + +#: ../melon/playlist/create.py:35 +msgid "Enter more playlist information" +msgstr "" + +#: ../melon/playlist/create.py:38 +msgid "Playlist Name" +msgstr "" + +#: ../melon/playlist/create.py:39 +msgid "Unnamed Playlist" +msgstr "" + +#: ../melon/playlist/create.py:41 +msgid "Playlist description" +msgstr "" + +#: ../melon/playlist/create.py:48 ../melon/playlist/pick.py:86 +msgid "Do not create playlist" +msgstr "" + +#: ../melon/playlist/create.py:49 +msgid "Create playlist" +msgstr "" + +#: ../melon/playlist/__init__.py:36 +msgid "You haven't added any videos to this playlist yet" +msgstr "" + +#: ../melon/playlist/__init__.py:38 +msgid "Start watching" +msgstr "" + +#: ../melon/playlist/pick.py:23 +msgid "Add to Playlist" +msgstr "" + +#: ../melon/playlist/pick.py:29 +msgid "The following video will be added to the playlist" +msgstr "" + +#: ../melon/playlist/pick.py:39 +msgid "Add to playlist" +msgstr "" + +#: ../melon/playlist/pick.py:40 +msgid "" +"Choose a playlist to add the video to. Note that you can only add videos to " +"local playlists, not external bookmarked ones" +msgstr "" + +#: ../melon/playlist/pick.py:42 +msgid "Create new playlist" +msgstr "" + +#: ../melon/playlist/pick.py:43 +msgid "Create a new playlist and add the video to it" +msgstr "" + +#: ../melon/import_providers/newpipe.py:28 +msgid "Newpipe Database importer" +msgstr "" + +#: ../melon/import_providers/newpipe.py:29 +msgid "" +"Import the .db file from inside the newpipe .zip export (as invidious " +"content)" +msgstr "" + +#: ../melon/import_providers/newpipe.py:30 +msgid "Select .db file" +msgstr "" + +#: ../melon/import_providers/newpipe.py:45 +msgid "Newpipe Database" +msgstr "" + +#: ../melon/importer.py:17 ../melon/importer.py:35 +msgid "Import" +msgstr "" + +#: ../melon/importer.py:36 +msgid "The following import methods have been found" +msgstr "" + +#: ../melon/importer.py:62 +msgid "There are no available importer methods" +msgstr "" + +#: ../melon/window.py:81 +msgid "Stream videos on the go" +msgstr "" diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..130707b --- /dev/null +++ b/po/meson.build @@ -0,0 +1,4 @@ +i18n = import('i18n') + +message('Update translations') +i18n.gettext(meson.project_name(), preset: 'glib') diff --git a/po/update_potfiles.sh b/po/update_potfiles.sh new file mode 100755 index 0000000..b1c968e --- /dev/null +++ b/po/update_potfiles.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# adapted from: https://codeberg.org/valos/Komikku/src/branch/main/po/update_potfiles.sh + +version=$(fgrep "version: " ../meson.build | grep -v "meson" | grep -o "'.*'" | sed "s/'//g") + +find ../melon -iname "*.py" | \ + xargs xgettext \ + --package-name=Melon \ + --package-version=$version \ + --from-code=UTF-8 \ + --output=melon.pot + +echo data/icu.ccw.Melon.desktop.in > POTFILES.in +find ../melon -iname "*.py" | \ + xargs -n 1 grep -H "import gettext" | \ + cut -d':' -f1 | \ + cut -d'/' -f2- >> POTFILES.in + +echo "# Please keep this list alphabetically sorted" > LINGUAS +for l in $(find -iname '*.po'); do basename $l .po >> LINGUAS; done + +for lang in $(sed "s/^#.*$//g" LINGUAS); do + mv "${lang}.po" "${lang}.po.old" + msginit --no-translator --locale=$lang --input melon.pot + mv "${lang}.po" "${lang}.po.new" + msgmerge -N "${lang}.po.old" "${lang}.po.new" > ${lang}.po + rm "${lang}.po.old" "${lang}.po.new" +done + +# To create language file use this command +# msginit --locale=LOCALE --input melon.pot +# where LOCALE is something like `de`, `it`, `es`... + +# To compile a .po file +# msgfmt --output-file=xx.mo xx.po +# where xx is something like `de`, `it`, `es`... -- 2.38.5