M README.md => README.md +8 -0
@@ 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.
M data/meson.build => data/meson.build +20 -1
@@ 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
#
M flatpak/icu.ccw.Melon.yml => flatpak/icu.ccw.Melon.yml +5 -5
@@ 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
M melon/browse/__init__.py => melon/browse/__init__.py +8 -7
@@ 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)
M melon/browse/channel.py => melon/browse/channel.py +6 -5
@@ 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
M melon/browse/playlist.py => melon/browse/playlist.py +3 -2
@@ 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))
M melon/browse/search.py => melon/browse/search.py +12 -13
@@ 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()
M melon/browse/server.py => melon/browse/server.py +11 -10
@@ 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:
M melon/home/history.py => melon/home/history.py +7 -6
@@ 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)
M melon/home/new.py => melon/home/new.py +10 -9
@@ 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)
M melon/home/playlists.py => melon/home/playlists.py +8 -7
@@ 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)
M melon/home/subs.py => melon/home/subs.py +7 -6
@@ 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)
M melon/import_providers/newpipe.py => melon/import_providers/newpipe.py +5 -4
@@ 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
M melon/importer.py => melon/importer.py +7 -6
@@ 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)
M melon/player/__init__.py => melon/player/__init__.py +7 -5
@@ 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")
M melon/playlist/__init__.py => melon/playlist/__init__.py +6 -4
@@ 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)
M melon/playlist/create.py => melon/playlist/create.py +11 -10
@@ 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
M melon/playlist/pick.py => melon/playlist/pick.py +9 -8
@@ 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)
M melon/servers/invidious/__init__.py => melon/servers/invidious/__init__.py +10 -8
@@ 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):
M melon/servers/nebula/__init__.py => melon/servers/nebula/__init__.py +18 -16
@@ 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"
M melon/servers/peertube/__init__.py => melon/servers/peertube/__init__.py +16 -14
@@ 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):
M melon/servers/utils.py => melon/servers/utils.py +0 -2
@@ 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
M melon/settings/__init__.py => melon/settings/__init__.py +17 -15
@@ 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)
M melon/widgets/preferencerow.py => melon/widgets/preferencerow.py +19 -16
@@ 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(
M melon/window.py => melon/window.py +3 -2
@@ 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):
M meson.build => meson.build +2 -0
@@ 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(
A po/LINGUAS => po/LINGUAS +1 -0
@@ 0,0 1,1 @@
+# Please keep this list alphabetically sorted
A po/POTFILES.in => po/POTFILES.in +22 -0
@@ 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
A po/melon.pot => po/melon.pot +536 -0
@@ 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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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 ""
A po/meson.build => po/meson.build +4 -0
@@ 0,0 1,4 @@
+i18n = import('i18n')
+
+message('Update translations')
+i18n.gettext(meson.project_name(), preset: 'glib')
A po/update_potfiles.sh => po/update_potfiles.sh +36 -0
@@ 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`...