From 9074fa3f35ce72c05b9242b8c5542b0218a5acb4 Mon Sep 17 00:00:00 2001 From: Joel Goguen Date: Sun, 10 Mar 2024 16:02:08 -0400 Subject: [PATCH] More code quality cleanups, don't need pygettext --- common.py | 17 +- container.py | 2 - conversion/input_config.py | 4 +- conversion/output_config.py | 44 +-- device/driver.py | 45 ++- device/koboextended_config.py | 10 +- pygettext.py | 628 ---------------------------------- scripts/build.sh | 2 +- scripts/update-calibre.py | 9 +- tests/assertions.py | 2 +- tests/test_common.py | 6 +- tests/test_container.py | 24 +- translations/messages.pot | 276 +++++++-------- 13 files changed, 218 insertions(+), 851 deletions(-) delete mode 100644 pygettext.py diff --git a/common.py b/common.py index 26d1560..9061c8e 100644 --- a/common.py +++ b/common.py @@ -135,10 +135,10 @@ def modify_epub( log.debug("Found meta node with name=cover") if cover_id: - log.info("Found cover image ID '{0}'".format(cover_id)) + log.info(f"Found cover image ID '{cover_id}'") cover_node_list: ElementBase = opf.xpath( - './opf:manifest/opf:item[@id="{0}"]'.format(cover_id), + f'./opf:manifest/opf:item[@id="{v}"]', namespaces=OPF_NAMESPACES, ) if len(cover_node_list) > 0: @@ -192,7 +192,7 @@ def modify_epub( nohyphen_css.name, name="kte-css/no-hyphens.css" ) ) - container.add_content_file_reference("kte-css/{0}".format(css_path)) + container.add_content_file_reference(f"kte-css/{css_path}") os.unlink(nohyphen_css.name) elif opts.get("hyphenate", False) and int(opts.get("hyphen_min_chars", 6)) > 0: if metadata and metadata.language == NULL_VALUES["language"]: @@ -220,7 +220,7 @@ def modify_epub( hyphen_css.name, name="kte-css/hyphenation.css" ) ) - container.add_content_file_reference("kte-css/{0}".format(css_path)) + container.add_content_file_reference(f"kte-css/{css_path}") os.unlink(hyphen_css.name) # Now smarten punctuation @@ -230,9 +230,8 @@ def modify_epub( if opts.get("extended_kepub_features", True): if metadata is not None: log.info( - "Adding extended Kobo features to {0} by {1}".format( - metadata.title, " and ".join(metadata.authors) - ) + "Adding extended Kobo features to {metadata.title} by " + + " and ".join(metadata.authors) ) # Add the Kobo span and div tags @@ -265,12 +264,12 @@ def modify_epub( stylehacks_css.name, name="kte-css/stylehacks.css" ) ) - container.add_content_file_reference("kte-css/{0}".format(css_path)) + container.add_content_file_reference(f"kte-css/{css_path}") os.unlink(filename) container.commit(filename) _modify_time = time.time() - _modify_start - log.info("modify_epub took {0:f} seconds".format(_modify_time)) + log.info(f"modify_epub took {_modify_time:0.2f} seconds") def intValueChanged(widget, singular, plural, *args, **kwargs): diff --git a/container.py b/container.py index 70e79d2..938437e 100644 --- a/container.py +++ b/container.py @@ -95,8 +95,6 @@ class InvalidEpub(ValueError): """Designates an invalid ePub file.""" - pass - class ParseError(ValueError): """Designates an error parsing an ePub inner file.""" diff --git a/conversion/input_config.py b/conversion/input_config.py index 163889c..5982818 100644 --- a/conversion/input_config.py +++ b/conversion/input_config.py @@ -76,6 +76,6 @@ def load_conversion_widgets(self): """Add our configuration to the input processing.""" super(OutputOptions, self).load_conversion_widgets() self.conversion_widgets.append(PluginWidget) - self.conversion_widgets = sorted( + self.conversion_widgets = sorted( # skipcq: PYL-W0201 self.conversion_widgets, key=lambda x: x.TITLE - ) # skipcq: PYL-W0201 + ) diff --git a/conversion/output_config.py b/conversion/output_config.py index 737ae48..7d58c11 100644 --- a/conversion/output_config.py +++ b/conversion/output_config.py @@ -75,9 +75,9 @@ def setupUi(self, Form): # noqa: N802, N803 self.opt_kepub_hyphenate.setText(_("Hyphenate Files")) # noqa: F821 self.gridLayout.addWidget(self.opt_kepub_hyphenate, rows, 0, 1, 1) - self.opt_kepub_disable_hyphenation = QtWidgets.QCheckBox( + self.opt_kepub_disable_hyphenation = QtWidgets.QCheckBox( # skipcq: PYL-W0201 Form - ) # skipcq: PYL-W0201 + ) self.opt_kepub_disable_hyphenation.setObjectName( "opt_kepub_disable_hyphenation" # noqa: F821 ) @@ -88,9 +88,9 @@ def setupUi(self, Form): # noqa: N802, N803 rows += 1 - self.opt_kepub_hyphenate_chars_label = QtWidgets.QLabel( + self.opt_kepub_hyphenate_chars_label = QtWidgets.QLabel( # skipcq: PYL-W0201 _("Minimum word length to hyphenate") + ":" # noqa: F821 - ) # skipcq: PYL-W0201 + ) self.gridLayout.addWidget(self.opt_kepub_hyphenate_chars_label, rows, 0, 1, 1) self.opt_kepub_hyphenate_chars = QtWidgets.QSpinBox(Form) # skipcq: PYL-W0201 @@ -109,16 +109,16 @@ def setupUi(self, Form): # noqa: N802, N803 rows += 1 - self.opt_kepub_hyphenate_chars_before_label = QtWidgets.QLabel( - _("Minimum characters before hyphens") + ":" # noqa: F821 - ) # skipcq: PYL-W0201 + self.opt_kepub_hyphenate_chars_before_label = ( # skipcq: PYL-W0201 + QtWidgets.QLabel(_("Minimum characters before hyphens") + ":") # noqa: F821 + ) self.gridLayout.addWidget( self.opt_kepub_hyphenate_chars_before_label, rows, 0, 1, 1 ) - self.opt_kepub_hyphenate_chars_before = QtWidgets.QSpinBox( + self.opt_kepub_hyphenate_chars_before = QtWidgets.QSpinBox( # skipcq: PYL-W0201 Form - ) # skipcq: PYL-W0201 + ) self.opt_kepub_hyphenate_chars_before_label.setBuddy( self.opt_kepub_hyphenate_chars_before ) @@ -138,16 +138,16 @@ def setupUi(self, Form): # noqa: N802, N803 rows += 1 - self.opt_kepub_hyphenate_chars_after_label = QtWidgets.QLabel( - _("Minimum characters after hyphens") + ":" # noqa: F821 - ) # skipcq: PYL-W0201 + self.opt_kepub_hyphenate_chars_after_label = ( # skipcq: PYL-W0201 + QtWidgets.QLabel(_("Minimum characters after hyphens") + ":") # noqa: F821 + ) self.gridLayout.addWidget( self.opt_kepub_hyphenate_chars_after_label, rows, 0, 1, 1 ) - self.opt_kepub_hyphenate_chars_after = QtWidgets.QSpinBox( + self.opt_kepub_hyphenate_chars_after = QtWidgets.QSpinBox( # skipcq: PYL-W0201 Form - ) # skipcq: PYL-W0201 + ) self.opt_kepub_hyphenate_chars_after_label.setBuddy( self.opt_kepub_hyphenate_chars_after ) @@ -167,16 +167,18 @@ def setupUi(self, Form): # noqa: N802, N803 rows += 1 - self.opt_kepub_hyphenate_limit_lines_label = QtWidgets.QLabel( - _("Maximum consecutive hyphenated lines") + ":" # noqa: F821 - ) # skipcq: PYL-W0201 + self.opt_kepub_hyphenate_limit_lines_label = ( # skipcq: PYL-W0201 + QtWidgets.QLabel( + _("Maximum consecutive hyphenated lines") + ":" # noqa: F821 + ) + ) self.gridLayout.addWidget( self.opt_kepub_hyphenate_limit_lines_label, rows, 0, 1, 1 ) - self.opt_kepub_hyphenate_limit_lines = QtWidgets.QSpinBox( + self.opt_kepub_hyphenate_limit_lines = QtWidgets.QSpinBox( # skipcq: PYL-W0201 Form - ) # skipcq: PYL-W0201 + ) self.opt_kepub_hyphenate_limit_lines_label.setBuddy( self.opt_kepub_hyphenate_limit_lines ) @@ -223,6 +225,6 @@ def load_conversion_widgets(self): """Add our configuration to the output process.""" super(OutputOptions, self).load_conversion_widgets() self.conversion_widgets.append(PluginWidget) - self.conversion_widgets = sorted( + self.conversion_widgets = sorted( # skipcq: PYL-W0201 self.conversion_widgets, key=lambda x: x.TITLE - ) # skipcq: PYL-W0201 + ) diff --git a/device/driver.py b/device/driver.py index 3fd4cbe..808e379 100644 --- a/device/driver.py +++ b/device/driver.py @@ -45,10 +45,8 @@ def __init__(self, name, author, message, fname=None, lineno=None): ValueError.__init__( self, _( # noqa: F821 - "Failed to parse '{book}' by '{author}' with error: '{error}' " - + "(file: {filename}, line: {lineno})" - ).format( - book=name, author=author, error=message, filename=fname, lineno=lineno + f"Failed to parse '{name}' by '{author}' with error: '{message}' " + + f"(file: {fname}, line: {lineno})" ), ) @@ -250,9 +248,8 @@ def _modify_epub(self, infile, metadata, container=None): o["kobotouchextended_currenttime"] = datetime.utcnow().ctime() kte_data_file = self.temporary_file("_KoboTouchExtendedDriverInfo") common.log.debug( - "KoboTouchExtended:_modify_epub:Driver data file :: {0}".format( - kte_data_file.name - ) + f"KoboTouchExtended:_modify_epub:Driver data file :: " + + kte_data_file.names ) kte_data_file.write(json.dumps(o).encode("UTF-8")) kte_data_file.close() @@ -276,10 +273,9 @@ def _modify_epub(self, infile, metadata, container=None): }, ) except Exception as e: - msg = "Failed to process {title} by {authors}: {msg}".format( - title=metadata.title, - authors=" and ".join(metadata.authors), - msg=str(e), + msg = ( + f"Failed to process {metadata.title} by " + + f"{' and '.join(metadata.authors)}: {e}" ) common.log.exception(msg) @@ -298,7 +294,7 @@ def _modify_epub(self, infile, metadata, container=None): dpath = self.create_upload_path(dpath, metadata, metadata.kte_calibre_name) common.log.info( "KoboTouchExtended:_modify_epub:Generated KePub file copy " - + "path: {0}".format(dpath) + + f"path: {dpath}" ) shutil.copy(infile, dpath) @@ -403,9 +399,10 @@ def upload_books(self, files, names, on_card=None, end_session=True, metadata=No cfg.add_section("FeatureSettings") common.log.info( "KoboTouchExtended:upload_books:Setting FeatureSettings." - + "FullBookPageNumbers to {0}".format( - "true" if self.full_page_numbers else "false" - ) + + "FullBookPageNumbers to " + + "true" + if self.full_page_numbers + else "false" ) cfg.set( "FeatureSettings", @@ -426,14 +423,10 @@ def filename_callback(self, path, mi): and path.endswith(EPUB_EXT) and mi.uuid not in self.skip_renaming_files ): - common.log.debug( - "KoboTouchExtended:filename_callback:Path - {0}".format(path) - ) + common.log.debug(f"KoboTouchExtended:filename_callback:Path - {path}") path = path[: -len(EPUB_EXT)] + KEPUB_EXT + EPUB_EXT - common.log.debug( - "KoboTouchExtended:filename_callback:New path - {0}".format(path) - ) + common.log.debug(f"KoboTouchExtended:filename_callback:New path - {path}") else: path = super(KOBOTOUCHEXTENDED, self).filename_callback(path, mi) @@ -449,10 +442,14 @@ def sync_booklists(self, booklists, end_session=True): common.log.info("KoboTouchExtended:sync_booklists:Setting ImageId fields") select_query = ( - "SELECT ContentId FROM content WHERE " - + "ContentType = ? AND " + # DeepSource picks this up as possible SQL injection from string + # concatenation, but the concatenation here is only static SQL. All + # parameters are parameterized and passed to the SQL engine for + # safe replacement. + "SELECT ContentId FROM content WHERE " # skipcq: BAN-B608 + + "ContentType = ? AND " # skipcq: BAN-B608 + "(ImageId IS NULL OR ImageId = '')" - ) # skipcq: BAN-B608 + ) update_query = "UPDATE content SET ImageId = ? WHERE ContentId = ?" try: db = self.device_database_connection() diff --git a/device/koboextended_config.py b/device/koboextended_config.py index beac88f..4560b2a 100644 --- a/device/koboextended_config.py +++ b/device/koboextended_config.py @@ -30,7 +30,7 @@ pass -def wrap_msg(msg): +def wrap_msg(msg: str) -> str: return textwrap.fill(msg.strip(), 100) @@ -267,11 +267,13 @@ def __init__(self, parent, device): self.skip_failed_checkbox = create_checkbox( _("Silently Ignore Failed Conversions"), # noqa: F821 _( # noqa: F821 - "Select this to not upload any book that fails conversion to " - + "kepub. If this is not selected, the upload process will be " + # DeepSource picks this up as possible SQL injection, which is not + # exactly correct. + "Select this to not upload any book that fails conversion to " # skipcq: BAN-B608 + + "kepub. If this is not selected, the upload process will be " # skipcq: BAN-B608 + "stopped at the first book that fails. If this is selected, " + "failed books will be silently removed from the upload queue." - ), # skipcq: BAN-B608 + ), device.get_pref("skip_failed"), ) diff --git a/pygettext.py b/pygettext.py deleted file mode 100644 index 683d7bc..0000000 --- a/pygettext.py +++ /dev/null @@ -1,628 +0,0 @@ -#! /usr/bin/env python -# -*- coding: iso-8859-1 -*- -# Originally written by Barry Warsaw -# -# Minimally patched to make it even more xgettext compatible -# by Peter Funk -# -# 2002-11-22 J�rgen Hermann -# Added checks that _() only contains string literals, and -# command line args are resolved to module lists, i.e. you -# can now pass a filename, a module or package name, or a -# directory (including globbing chars, important for Win32). -# Made docstring fit in 80 chars wide displays using pydoc. -# - -"""Generates gettext templates.""" - -import getopt -import importlib.machinery -import operator -import os -import sys -import time -import token -import tokenize -from functools import reduce -from typing import List - -# for selftesting -try: - import fintl - - _ = fintl.gettext -except ImportError: - - def _(s): - return s - - -__doc__ = _( - """pygettext -- Python equivalent of xgettext(1) - -Many systems (Solaris, Linux, Gnu) provide extensive tools that ease the -internationalization of C programs. Most of these tools are independent of -the programming language and can be used from within Python programs. -Martin von Loewis' work[1] helps considerably in this regard. - -There's one problem though; xgettext is the program that scans source code -looking for message strings, but it groks only C (or C++). Python -introduces a few wrinkles, such as dual quoting characters, triple quoted -strings, and raw strings. xgettext understands none of this. - -Enter pygettext, which uses Python's standard tokenize module to scan -Python source code, generating .pot files identical to what GNU xgettext[2] -generates for C and C++ code. From there, the standard GNU tools can be -used. - -A word about marking Python strings as candidates for translation. GNU -xgettext recognizes the following keywords: gettext, dgettext, dcgettext, -and gettext_noop. But those can be a lot of text to include all over your -code. C and C++ have a trick: they use the C preprocessor. Most -internationalized C source includes a #define for gettext() to _() so that -what has to be written in the source is much less. Thus these are both -translatable strings: - - gettext("Translatable String") - _("Translatable String") - -Python of course has no preprocessor so this doesn't work so well. Thus, -pygettext searches only for _() by default, but see the -k/--keyword flag -below for how to augment this. - - [1] http://www.python.org/workshops/1997-10/proceedings/loewis.html - [2] http://www.gnu.org/software/gettext/gettext.html - -NOTE: pygettext attempts to be option and feature compatible with GNU -xgettext where ever possible. However some options are still missing or are -not fully implemented. Also, xgettext's use of command line switches with -option arguments is broken, and in these cases, pygettext just defines -additional switches. - -Usage: pygettext [options] inputfile ... - -Options: - - -a - --extract-all - Extract all strings. - - -d name - --default-domain=name - Rename the default output file from messages.pot to name.pot. - - -E - --escape - Replace non-ASCII characters with octal escape sequences. - - -D - --docstrings - Extract module, class, method, and function docstrings. These do - not need to be wrapped in _() markers, and in fact cannot be for - Python to consider them docstrings. (See also the -X option). - - -h - --help - Print this help message and exit. - - -k word - --keyword=word - Keywords to look for in addition to the default set, which are: - %(DEFAULTKEYWORDS)s - - You can have multiple -k flags on the command line. - - -K - --no-default-keywords - Disable the default set of keywords (see above). Any keywords - explicitly added with the -k/--keyword option are still recognized. - - --no-location - Do not write filename/lineno location comments. - - -n - --add-location - Write filename/lineno location comments indicating where each - extracted string is found in the source. These lines appear before - each msgid. The style of comments is controlled by the -S/--style - option. This is the default. - - -o filename - --output=filename - Rename the default output file from messages.pot to filename. If - filename is `-' then the output is sent to standard out. - - -p dir - --output-dir=dir - Output files will be placed in directory dir. - - -S stylename - --style stylename - Specify which style to use for location comments. Two styles are - supported: - - Solaris # File: filename, line: line-number - GNU #: filename:line - - The style name is case insensitive. GNU style is the default. - - -v - --verbose - Print the names of the files being processed. - - -V - --version - Print the version of pygettext and exit. - - -w columns - --width=columns - Set width of output to columns. - - -x filename - --exclude-file=filename - Specify a file that contains a list of strings that are not be - extracted from the input files. Each string to be excluded must - appear on a line by itself in the file. - - -X filename - --no-docstrings=filename - Specify a file that contains a list of files (one per line) that - should not have their docstrings extracted. This is only useful in - conjunction with the -D option above. - -If `inputfile' is -, standard input is read. -""" -) - -__version__ = "1.5" - -default_keywords = ["_"] -DEFAULTKEYWORDS = ", ".join(default_keywords) - -EMPTYSTRING = "" - -# The normal pot-file header. msgmerge and Emacs's po-mode work better if it's -# there. -pot_header = _( - """\ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR ORGANIZATION -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\\n" -"POT-Creation-Date: %(time)s\\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n" -"Last-Translator: FULL NAME \\n" -"Language-Team: LANGUAGE \\n" -"MIME-Version: 1.0\\n" -"Content-Type: text/plain; charset=CHARSET\\n" -"Content-Transfer-Encoding: ENCODING\\n" -"Generated-By: pygettext.py %(version)s\\n" - -""" -) - - -def usage(code, msg=""): - """Print script usage.""" - print(__doc__ % globals(), file=sys.stderr) - if msg: - print(msg, file=sys.stderr) - sys.exit(code) - - -escapes = [] - - -def make_escapes(pass_iso8859): - """Escape any non-ASCII character.""" - if pass_iso8859: - # Allow iso-8859 characters to pass through so that e.g. 'msgid - # "H�he"' would result not result in 'msgid "H\366he"'. Otherwise we - # escape any character outside the 32..126 range. - mod = 128 - else: - mod = 256 - for i in range(256): - if 32 <= (i % mod) <= 126: - escapes.append(chr(i)) - else: - escapes.append("\\%03o" % i) - escapes[ord("\\")] = "\\\\" - escapes[ord("\t")] = "\\t" - escapes[ord("\r")] = "\\r" - escapes[ord("\n")] = "\\n" - escapes[ord('"')] = '\\"' - - -def escape(s): - """Process escapes for a string.""" - s = list(s) - for i in range(len(s)): - s[i] = escapes[ord(s[i])] - return EMPTYSTRING.join(s) - - -def safe_eval(s): - """Unwrap quotes, safely.""" - return eval(s, {"__builtins__": {}}, {}) - - -def normalize(s): - """Convert Python string types into a format appropriate for .po files.""" - lines = s.split("\n") - if len(lines) == 1: - s = '"' + escape(s) + '"' - else: - if not lines[-1]: - del lines[-1] - lines[-1] = lines[-1] + "\n" - for i in range(len(lines)): - lines[i] = escape(lines[i]) - lineterm = '\\n"\n"' - s = '""\n"' + lineterm.join(lines) + '"' - return s - - -def contains_any(haystack, needles): - """Check whether 'haystack' contains ANY of the chars in 'needles'.""" - return 1 in [c in haystack for c in needles] - - -def get_files_for_name(name): - """Get a list of module files. - - Fetches a list of module files for a filename, a module or package name, or - a directory. - """ - if os.path.isdir(name): - # find all python files in directory - file_list = [] - for root, _, files in os.walk(name): - for fname in files: - if os.path.splitext(fname)[-1] in importlib.machinery.SOURCE_SUFFIXES: - file_list.append(os.path.join(root, fname)) - return file_list - - if ( - os.path.exists(name) - and os.path.splitext(name)[-1] in importlib.machinery.SOURCE_SUFFIXES - ): - # a single file - return [name] - - return [] - - -class TokenEater: - """Likes to eat tokens.""" - - def __init__(self, options): - """Initialize a TokenEater.""" - self.__options = options - self.__messages = {} - self.__state = self.__waiting - self.__data = [] - self.__lineno = -1 - self.__freshmodule = 1 - self.__curfile = None - - def __call__(self, ttype, tstring, stup, etup, line): - """Allow this class to be called like a function.""" - self.__state(ttype, tstring, line) - - def __waiting(self, ttype, tstring, lineno): - """Get the waiting state.""" - opts = self.__options - # Do docstring extractions, if enabled - if opts.docstrings and not opts.nodocstrings.get(self.__curfile): - # module docstring? - if self.__freshmodule: - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__freshmodule = 0 - elif ttype not in (tokenize.COMMENT, tokenize.NL): - self.__freshmodule = 0 - return - # class docstring? - if ttype == tokenize.NAME and tstring in ("class", "def"): - self.__state = self.__suiteseen - return - if ttype == tokenize.NAME and tstring in opts.keywords: - self.__state = self.__keywordseen - - def __suiteseen(self, ttype, tstring, lineno): # skipcq: PYL-W0613 - # ignore anything until we see the colon - if ttype == tokenize.OP and tstring == ":": - self.__state = self.__suitedocstring - - def __suitedocstring(self, ttype, tstring, lineno): - # ignore any intervening noise - if ttype == tokenize.STRING: - self.__addentry(safe_eval(tstring), lineno, isdocstring=1) - self.__state = self.__waiting - elif ttype not in (tokenize.NEWLINE, tokenize.INDENT, tokenize.COMMENT): - # there was no class docstring - self.__state = self.__waiting - - def __keywordseen(self, ttype, tstring, lineno): - if ttype == tokenize.OP and tstring == "(": - self.__data = [] - self.__lineno = lineno - self.__state = self.__openseen - else: - self.__state = self.__waiting - - def __openseen(self, ttype, tstring, lineno): # skipcq: PYL-W0613 - if ttype == tokenize.OP and tstring == ")": - # We've seen the last of the translatable strings. Record the - # line number of the first line of the strings and update the list - # of messages seen. Reset state for the next batch. If there - # were no strings inside _(), then just ignore this entry. - if self.__data: - self.__addentry(EMPTYSTRING.join(self.__data)) - self.__state = self.__waiting - elif ttype == tokenize.STRING: - self.__data.append(safe_eval(tstring)) - elif ttype not in [ - tokenize.COMMENT, - token.INDENT, - token.DEDENT, - token.NEWLINE, - tokenize.NL, - ]: - # warn if we see anything else than STRING or whitespace - print( - _( - '*** {file}:{lineno}: Seen unexpected token "{token}"'.format( - token=tstring, file=self.__curfile, lineno=self.__lineno - ) - ) - ) - self.__state = self.__waiting - - def __addentry(self, msg, lineno=None, isdocstring=0): - if lineno is None: - lineno = self.__lineno - if msg not in self.__options.toexclude: - entry = (self.__curfile, lineno) - self.__messages.setdefault(msg, {})[entry] = isdocstring - - def set_filename(self, filename): - """Set the current filename.""" - self.__curfile = filename - self.__freshmodule = 1 - - def write(self, fp): - """Write the gettext template.""" - options = self.__options - timestamp = time.strftime("%Y-%m-%d %H:%M+%Z") - # The time stamp in the header doesn't have the same format as that - # generated by xgettext... - print(pot_header % {"time": timestamp, "version": __version__}, file=fp) - # Sort the entries. First sort each particular entry's keys, then - # sort all the entries by their first item. - reverse = {} - for k, v in list(self.__messages.items()): - keys = sorted(v.keys()) - reverse.setdefault(tuple(keys), []).append((k, v)) - rkeys = sorted(reverse.keys()) - for rkey in rkeys: - rentries = sorted(reverse[rkey]) - for k, v in rentries: - isdocstring = 0 - # If the entry was gleaned out of a docstring, then add a - # comment stating so. This is to aid translators who may wish - # to skip translating some unimportant docstrings. - if reduce(operator.__add__, list(v.values())): - isdocstring = 1 - # k is the message string, v is a dictionary-set of (filename, - # lineno) tuples. We want to sort the entries in v first by - # file name and then by line number. - v = sorted(v.keys()) - if not options.writelocations: - pass - # location comments are different b/w Solaris and GNU: - elif options.locationstyle == options.SOLARIS: - for filename, lineno in v: - d = {"filename": filename, "lineno": lineno} - print(_("# File: %(filename)s, line: %(lineno)d") % d, file=fp) - elif options.locationstyle == options.GNU: - # fit as many locations on one line, as long as the - # resulting line length doesn't exceeds 'options.width' - locline = "#:" - for filename, lineno in v: - d = {"filename": filename, "lineno": lineno} - s = _(" %(filename)s:%(lineno)d") % d - if len(locline) + len(s) <= options.width: - locline = locline + s - else: - print(locline, file=fp) - locline = "#:" + s - if len(locline) > 2: - print(locline, file=fp) - if isdocstring: - print("#, docstring", file=fp) - print("msgid", normalize(k), file=fp) - print('msgstr ""\n', file=fp) - - -def main(): - """Run the script.""" - opts = () - args = [] - try: - opts, args = getopt.getopt( - sys.argv[1:], - "ad:DEhk:Kno:p:S:Vvw:x:X:", - [ - "extract-all", - "default-domain=", - "escape", - "help", - "keyword=", - "no-default-keywords", - "add-location", - "no-location", - "output=", - "output-dir=", - "style=", - "verbose", - "version", - "width=", - "exclude-file=", - "docstrings", - "no-docstrings", - ], - ) - except getopt.error as msg: - usage(1, str(msg)) - - # for holding option values - class Options: - # constants - GNU = 1 - SOLARIS = 2 - # defaults - extractall = 0 # FIXME: currently this option has no effect at all. - escape = 0 - keywords: List[str] = [] - outpath = "" - outfile = "messages.pot" - writelocations = 1 - locationstyle = GNU - verbose = 0 - width = 78 - excludefilename = "" - toexclude: List[str] = [] - docstrings = 0 - nodocstrings = {} - - options = Options() - locations = {"gnu": options.GNU, "solaris": options.SOLARIS} - - # parse options - for opt, arg in opts: - if opt in ("-h", "--help"): - usage(0) - elif opt in ("-a", "--extract-all"): - options.extractall = 1 - elif opt in ("-d", "--default-domain"): - options.outfile = arg + ".pot" - elif opt in ("-E", "--escape"): - options.escape = 1 - elif opt in ("-D", "--docstrings"): - options.docstrings = 1 - elif opt in ("-k", "--keyword"): - options.keywords.append(arg) - elif opt in ("-n", "--add-location"): - options.writelocations = 1 - elif opt in ("--no-location",): - options.writelocations = 0 - elif opt in ("-S", "--style"): - options.locationstyle = locations.get(arg.lower(), 1) - elif opt in ("-o", "--output"): - options.outfile = arg - elif opt in ("-p", "--output-dir"): - options.outpath = arg - elif opt in ("-v", "--verbose"): - options.verbose = 1 - elif opt in ("-V", "--version"): - print(_("pygettext.py (xgettext for Python) {0}").format(__version__)) - sys.exit(0) - elif opt in ("-w", "--width"): - try: - options.width = int(arg) - except ValueError: - usage(1, _("--width argument must be an integer: %s") % arg) - elif opt in ("-x", "--exclude-file"): - options.excludefilename = arg - elif opt in ("-X", "--no-docstrings"): - fp = open(arg) - try: - while True: - line = fp.readline() - if not line: - break - options.nodocstrings[line[:-1]] = 1 - finally: - fp.close() - - # calculate escapes - make_escapes(options.escape) - - # calculate all keywords - options.keywords.extend(default_keywords) - - # initialize list of strings to exclude - if options.excludefilename: - try: - with open(options.excludefilename) as fp: - options.toexclude = fp.readlines() - except IOError: - print( - _("Can't read --exclude-file: {0}").format(options.excludefilename), - file=sys.stderr, - ) - sys.exit(1) - else: - options.toexclude = [] - - # resolve args to module lists - expanded = [] - for arg in args: - if arg == "-": - expanded.append(arg) - else: - expanded.extend(get_files_for_name(arg)) - args = expanded - - # slurp through all the files - eater = TokenEater(options) - for filename in args: - if filename == "-": - if options.verbose: - print(_("Reading standard input")) - fp = sys.stdin - closep = 0 - else: - if options.verbose: - print(_("Working on {0}").format(filename)) - fp = open(filename, "rt") - closep = 1 - try: - eater.set_filename(filename) - try: - for tok in tokenize.generate_tokens(fp.readline): - eater(*tok) - except tokenize.TokenError as e: - print(f"{e}", file=sys.stderr) - finally: - if closep: - fp.close() - - # write the output - if options.outfile == "-": - fp = sys.stdout - closep = 0 - else: - if options.outpath: - options.outfile = os.path.join(options.outpath, options.outfile) - fp = open(options.outfile, "w") - closep = 1 - try: - eater.write(fp) - finally: - if closep: - fp.close() - - -if __name__ == "__main__": - main() - # some more test strings - # _("a unicode string") - # this one creates a warning - # _('*** Seen unexpected token "%(token)s"') % {"token": "test"} - # _("more" "than" "one" "string") diff --git a/scripts/build.sh b/scripts/build.sh index 87321e4..3e18ee0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -41,7 +41,7 @@ make_pot() { # shellcheck disable=SC2046 "${XGETTEXT_BIN}" -o ./translations/messages.pot --no-wrap \ --copyright-holder="Joel Goguen" --package-name="calibre-kobo-driver" \ - $(/usr/bin/find . -type f -name '*.py' -not \( -name 'pygettext.py' -or -name 'test_*.py' -or -path '*/calibre-*' \)) + $(/usr/bin/find . -type f -name '*.py' -not \( -name 'test_*.py' -or -path '*/calibre-*' \)) } # Equivalent of `make clean` diff --git a/scripts/update-calibre.py b/scripts/update-calibre.py index 6eb6c0f..29917f5 100755 --- a/scripts/update-calibre.py +++ b/scripts/update-calibre.py @@ -106,11 +106,14 @@ def get_calibre() -> None: for asset in release_data["assets"]: if asset["name"].endswith(PKG_EXT): print(f"Found desired asset name: {asset['name']}") - if not asset["browser_download_url"].startswith("http"): + if asset["browser_download_url"].lower().startswith("http"): + pkg_resp = urlopen(asset["browser_download_url"]) + else: raise ValueError( - f"Browser download URL does not begin with http/https: {asset['browser_download_url']}" + f"Browser download URL does not begin with http/https: " + + asset["browser_download_url"] ) - pkg_resp = urlopen(asset["browser_download_url"]) + if pkg_resp.status != 200: raise Exception( "Calibre download returned " diff --git a/tests/assertions.py b/tests/assertions.py index 9cb5bd7..97aa337 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -51,4 +51,4 @@ def assertIsNoneOrEmptyString(self, value: Optional[Any]): # value is not None and is a string type if value.strip() != "": # value is not empty - self.fail("expected empty string, got: {0}".format(value)) + self.fail(f"expected empty string, got: {value}") diff --git a/tests/test_common.py b/tests/test_common.py index 4bc7c02..8141614 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -101,11 +101,7 @@ def test_logger_ensure_unicode_from_bytes(self) -> None: test_tagged = logger._tag_args("DEBUG", msg) self.assertListEqual( test_tagged, - [ - "{timestr} [{level}] {msg}".format( - timestr=TEST_TIME, level="DEBUG", msg=msg - ), - ], + [f"{TEST_TIME} [DEBUG] {msg}"], ) @mock.patch( diff --git a/tests/test_container.py b/tests/test_container.py index 1e205ce..d87543d 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -18,9 +18,7 @@ test_dir = os.path.dirname(os.path.abspath(__file__)) src_dir = os.path.dirname(test_dir) -test_libdir = os.path.join( - src_dir, "pylib", "python{major}".format(major=sys.version_info.major) -) +test_libdir = os.path.join(src_dir, "pylib", f"python{sys.version_info.major}") sys.path = [src_dir] + glob.glob(os.path.join(test_libdir, "*.zip")) + sys.path from tests.assertions import TestAssertions @@ -123,7 +121,7 @@ def test_divs_added(self): o = self.container.parsed(os.path.basename(self.files["test_without_spans"])) for div_id in {"book-columns", "book-inner"}: element_count = o.xpath( - 'count(//xhtml:div[@id="{0}"])'.format(div_id), + f'count(//xhtml:div[@id="{div_id}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(element_count, 1) @@ -133,7 +131,7 @@ def test_not_adding_divs_twice(self): o = self.container.parsed(os.path.basename(self.files["test_with_spans"])) for div_id in {"book-columns", "book-inner"}: element_count = o.xpath( - 'count(//xhtml:div[@id="{0}"])'.format(div_id), + f'count(//xhtml:div[@id="{div_id}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(element_count, 1) @@ -206,7 +204,7 @@ def test_add_css(self): html = self.container.parsed(html_container_name) css_pre_count = html.xpath( - 'count(//xhtml:head/xhtml:style[@href="{0}"])'.format(css_container_name), + f'count(//xhtml:head/xhtml:style[@href="{css_container_name}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(css_pre_count, 0) @@ -214,14 +212,14 @@ def test_add_css(self): self.container.add_content_file_reference(css_container_name) html = self.container.parsed(html_container_name) css_post_count = html.xpath( - 'count(//xhtml:head/xhtml:link[@href="{0}"])'.format(css_container_name), + f'count(//xhtml:head/xhtml:link[@href="{css_container_name}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(css_post_count, 1) self.assertIsNotNone( re.search( - r''.format(css_container_name), + rf'', self.container.raw_data(html_container_name), re.UNICODE | re.MULTILINE, ) @@ -235,7 +233,7 @@ def test_add_js(self): html = self.container.parsed(html_container_name) js_pre_count = html.xpath( - 'count(//xhtml:head/xhtml:script[@src="{0}"])'.format(js_container_name), + f'count(//xhtml:head/xhtml:script[@src="{js_container_name}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(js_pre_count, 0) @@ -243,14 +241,14 @@ def test_add_js(self): self.container.add_content_file_reference(js_container_name) html = self.container.parsed(html_container_name) js_post_count = html.xpath( - 'count(//xhtml:head/xhtml:script[@src="{0}"])'.format(js_container_name), + f'count(//xhtml:head/xhtml:script[@src="{js_container_name}"])', namespaces={"xhtml": container.XHTML_NAMESPACE}, ) self.assertEqual(js_post_count, 1) self.assertIsNotNone( re.search( - r''.format(js_container_name), + rf'', self.container.raw_data(html_container_name), re.UNICODE | re.MULTILINE, ) @@ -268,7 +266,7 @@ def test_add_spans_to_text(self): for text in text_samples: for text_only in {True, False}: self.container.paragraph_counter = defaultdict(lambda: 1) - node = etree.Element("{{{0}}}p".format(container.XHTML_NAMESPACE)) + node = etree.Element(f"{{{container.XHTML_NAMESPACE}}}p") if text_only: self.assertTrue( @@ -292,7 +290,7 @@ def test_add_spans_to_text(self): def __run_multiple_node_test(self, text_nodes): # type: (List[str]) -> None html = "
" for text in text_nodes: - html += "

{0}

".format(text) + html += f"

{text}

" html += "
" node = etree.fromstring(html) self.container._paragraph_counter = 1 diff --git a/translations/messages.pot b/translations/messages.pot index 4d4efd4..23e3def 100644 --- a/translations/messages.pot +++ b/translations/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: calibre-kobo-driver\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-10-11 10:54-0700\n" +"POT-Creation-Date: 2024-03-10 16:01-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,272 +18,223 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -#: container.py:204 -msgid "A source path must be given" -msgstr "" - -#: container.py:237 -#, python-brace-format -msgid "A valid file name must be given (got {filename})" -msgstr "" - -#: container.py:249 -#, python-brace-format -msgid "Could not retrieve content file {0}" -msgstr "" - -#: container.py:256 -#, python-brace-format -msgid "Could not find a element in {0}" -msgstr "" - -#: container.py:263 -#, python-brace-format -msgid "A section was found but was undefined in content file {0}" -msgstr "" - -#: container.py:319 container.py:355 -#, python-brace-format -msgid "No HTML content in {0}" -msgstr "" - -#: container.py:381 -#, python-brace-format -msgid "No HTML content in file {0}" -msgstr "" - -#: container.py:436 container.py:460 container.py:534 -#, python-brace-format -msgid "Skipping file {fname}" -msgstr "" - -#: container.py:440 -msgid "Kobo
tag present" -msgid_plural "Kobo
tags present" -msgstr[0] "" -msgstr[1] "" - -#: container.py:464 -#, python-brace-format -msgid "{div_count:d}
tag" -msgid_plural "{div_count:d}
tags" -msgstr[0] "" -msgstr[1] "" - -#: container.py:468 -#, python-brace-format -msgid "{p_count:d}

tag" -msgid_plural "{p_count:d}

tags" -msgstr[0] "" -msgstr[1] "" - -#: container.py:538 -msgid "Kobo tag present" -msgid_plural "Kobo tags present" -msgstr[0] "" -msgstr[1] "" - -#: conversion/input_config.py:40 +#: conversion/input_config.py:35 msgid "Options specific to KePub input." msgstr "" -#: conversion/input_config.py:65 +#: conversion/input_config.py:60 msgid "Strip Kobo spans" msgstr "" -#: conversion/kepub_input.py:40 +#: conversion/kepub_input.py:39 msgid "Kepubs have spans wrapping each sentence. These are used by the ereader for the reading location and bookmark location. They are not used by an ePub reader but are valid code and can be safely be left in the ePub. If you plan to edit the ePub, it is recommended that you remove the spans." msgstr "" -#: conversion/kepub_input.py:100 +#: conversion/kepub_input.py:105 #, python-brace-format msgid "{0} is not a valid KEPUB file (could not find opf)" msgstr "" -#: conversion/kepub_input.py:133 +#: conversion/kepub_input.py:135 msgid "EPUB files with DTBook markup are not supported" msgstr "" -#: conversion/kepub_input.py:158 +#: conversion/kepub_input.py:160 msgid "No valid entries in the spine of this EPUB" msgstr "" -#: conversion/kepub_output.py:50 +#: conversion/kepub_output.py:53 msgid "Select this to add a CSS file which enables hyphenation." msgstr "" -#: conversion/kepub_output.py:53 +#: conversion/kepub_output.py:56 msgid "The language used will be the language defined for the book in calibre." msgstr "" -#: conversion/kepub_output.py:57 +#: conversion/kepub_output.py:60 msgid "Please see the README file for directions on updating hyphenation dictionaries." msgstr "" -#: conversion/kepub_output.py:69 +#: conversion/kepub_output.py:72 msgid "Select this to disable all hyphenation in a book." msgstr "" -#: conversion/kepub_output.py:72 +#: conversion/kepub_output.py:75 msgid "This takes precedence over the hyphenation option." msgstr "" -#: conversion/kepub_output.py:80 device/koboextended_config.py:164 +#: conversion/kepub_output.py:83 device/koboextended_config.py:134 msgid "Select this to clean up the internal ePub markup." msgstr "" -#: conversion/kepub_output.py:86 +#: conversion/kepub_output.py:89 msgid "Sets the minimum word length, in characters, for hyphenation to be allowed." msgstr "" -#: conversion/kepub_output.py:94 +#: conversion/kepub_output.py:97 msgid "Sets the minimum number of characters which must appear before a hyphen" msgstr "" -#: conversion/kepub_output.py:102 +#: conversion/kepub_output.py:105 msgid "Sets the minimum number of characters which must appear after a hyphen" msgstr "" -#: conversion/kepub_output.py:111 +#: conversion/kepub_output.py:114 msgid "Sets the maximum number of consecutive lines that may be hyphenated." msgstr "" -#: conversion/kepub_output.py:114 +#: conversion/kepub_output.py:117 msgid "Set this to 0 to disable limiting." msgstr "" -#: conversion/output_config.py:32 +#: conversion/output_config.py:30 msgid "Options specific to KePub output" msgstr "" -#: conversion/output_config.py:81 device/koboextended_config.py:146 +#: conversion/output_config.py:75 device/koboextended_config.py:361 msgid "Hyphenate Files" msgstr "" -#: conversion/output_config.py:89 device/koboextended_config.py:200 +#: conversion/output_config.py:85 device/koboextended_config.py:372 msgid "Disable hyphenation" msgstr "" -#: conversion/output_config.py:96 device/koboextended_config.py:206 +#: conversion/output_config.py:92 device/koboextended_config.py:378 msgid "Minimum word length to hyphenate" msgstr "" -#: conversion/output_config.py:103 conversion/output_config.py:185 -#: device/koboextended_config.py:212 device/koboextended_config.py:283 +#: conversion/output_config.py:99 conversion/output_config.py:189 +#: device/koboextended_config.py:384 device/koboextended_config.py:455 msgid "Disabled" msgstr "" -#: conversion/output_config.py:108 conversion/output_config.py:134 -#: conversion/output_config.py:161 device/koboextended_config.py:217 -#: device/koboextended_config.py:238 device/koboextended_config.py:262 +#: conversion/output_config.py:104 conversion/output_config.py:132 +#: conversion/output_config.py:161 device/koboextended_config.py:389 +#: device/koboextended_config.py:410 device/koboextended_config.py:434 msgid "character" msgstr "" -#: conversion/output_config.py:109 conversion/output_config.py:135 -#: conversion/output_config.py:162 device/koboextended_config.py:218 -#: device/koboextended_config.py:239 device/koboextended_config.py:263 +#: conversion/output_config.py:105 conversion/output_config.py:133 +#: conversion/output_config.py:162 device/koboextended_config.py:390 +#: device/koboextended_config.py:411 device/koboextended_config.py:435 msgid "characters" msgstr "" -#: conversion/output_config.py:117 device/koboextended_config.py:224 +#: conversion/output_config.py:113 device/koboextended_config.py:396 msgid "Minimum characters before hyphens" msgstr "" -#: conversion/output_config.py:144 device/koboextended_config.py:248 +#: conversion/output_config.py:142 device/koboextended_config.py:420 msgid "Minimum characters after hyphens" msgstr "" -#: conversion/output_config.py:171 device/koboextended_config.py:272 +#: conversion/output_config.py:172 device/koboextended_config.py:444 msgid "Maximum consecutive hyphenated lines" msgstr "" -#: conversion/output_config.py:191 device/koboextended_config.py:289 +#: conversion/output_config.py:195 device/koboextended_config.py:461 msgid "line" msgstr "" -#: conversion/output_config.py:192 device/koboextended_config.py:290 +#: conversion/output_config.py:196 device/koboextended_config.py:462 msgid "lines" msgstr "" -#: conversion/output_config.py:203 +#: conversion/output_config.py:207 msgid "Clean up ePub markup" msgstr "" +#: device/driver.py:48 +#, python-brace-format +msgid "Failed to parse '{name}' by '{author}' with error: '{message}' " +msgstr "" + +#: device/driver.py:69 +msgid "Communicate with Kobo Touch and later firmwares to enable extended Kobo ePub features." +msgstr "" + #: device/koboextended_config.py:66 msgid "Extended" msgstr "" -#: device/koboextended_config.py:111 -msgid "Extended driver" +#: device/koboextended_config.py:119 +msgid "Other options" msgstr "" -#: device/koboextended_config.py:119 -msgid "Enable Extended Kobo Features" +#: device/koboextended_config.py:127 +msgid "Smarten Punctuation" msgstr "" -#: device/koboextended_config.py:120 -msgid "Choose whether to enable extra customizations" +#: device/koboextended_config.py:128 +msgid "Select this to smarten punctuation in the ePub" msgstr "" -#: device/koboextended_config.py:125 -msgid "Upload DRM-encumbered ePub files" +#: device/koboextended_config.py:133 +msgid "Clean up ePub Markup" msgstr "" -#: device/koboextended_config.py:127 -msgid "Select this to upload ePub files encumbered by DRM. If this is not selected, it is a fatal error to upload an encumbered file" +#: device/koboextended_config.py:139 +msgid "Use full book page numbers" msgstr "" -#: device/koboextended_config.py:135 -msgid "Silently Ignore Failed Conversions" +#: device/koboextended_config.py:141 +msgid "Select this to show page numbers for the whole book, instead of each chapter. This will also affect regular ePub page number display! This is only useful for firmware before 3.11.0." msgstr "" -#: device/koboextended_config.py:137 -msgid "Select this to not upload any book that fails conversion to kepub. If this is not selected, the upload process will be stopped at the first book that fails. If this is selected, failed books will be silently removed from the upload queue." +#: device/koboextended_config.py:221 +msgid "Send books as kepubs" msgstr "" -#: device/koboextended_config.py:148 -msgid "Select this to add a CSS file which enables hyphenation. The language used will be the language defined for the book in calibre. Please see the README file for directions on updating hyphenation dictionaries." +#: device/koboextended_config.py:232 +msgid "Enable options to transform books to kepubs when sending them to the device." msgstr "" -#: device/koboextended_config.py:157 -msgid "Smarten Punctuation" +#: device/koboextended_config.py:239 +msgid "Use template for kepubification" msgstr "" -#: device/koboextended_config.py:158 -msgid "Select this to smarten punctuation in the ePub" +#: device/koboextended_config.py:241 +msgid "Use a template to decide if books should be kepubified. If result is false or blank, it will not be kepubified." msgstr "" -#: device/koboextended_config.py:163 -msgid "Clean up ePub Markup" +#: device/koboextended_config.py:249 +msgid "Enter a template to decide if a book is to be kepubified. If the template returns false or true, the book will not be kepubified and not other modifications will be made to the book." msgstr "" -#: device/koboextended_config.py:169 device/koboextended_config.py:177 -msgid "Copy generated KePub files to a directory" +#: device/koboextended_config.py:258 +msgid "Upload DRM-encumbered ePub files" msgstr "" -#: device/koboextended_config.py:171 device/koboextended_config.py:182 -msgid "Enter an absolute directory path to copy all generated KePub files into for debugging purposes." +#: device/koboextended_config.py:260 +msgid "Select this to upload ePub files encumbered by DRM. If this is not selected, it is a fatal error to upload an encumbered file" msgstr "" -#: device/koboextended_config.py:190 -msgid "Use full book page numbers" +#: device/koboextended_config.py:268 +msgid "Silently Ignore Failed Conversions" msgstr "" -#: device/koboextended_config.py:192 -msgid "Select this to show page numbers for the whole book, instead of each chapter. This will also affect regular ePub page number display!" +#: device/koboextended_config.py:272 +msgid "Select this to not upload any book that fails conversion to kepub. If this is not selected, the upload process will be stopped at the first book that fails. If this is selected, failed books will be silently removed from the upload queue." msgstr "" -#: device/koboextended_config.py:201 -msgid "Select this to disable hyphenation for books." +#: device/koboextended_config.py:281 device/koboextended_config.py:289 +msgid "Copy generated KePub files to a directory" msgstr "" -#: device/driver.py:56 -#, python-brace-format -msgid "Failed to parse '{book}' by '{author}' with error: '{error}' (file: {filename}, line: {lineno})" +#: device/koboextended_config.py:283 device/koboextended_config.py:294 +msgid "Enter an absolute directory path to copy all generated KePub files into for debugging purposes." msgstr "" -#: device/driver.py:79 -msgid "Communicate with Kobo Touch and later firmwares to enable extended Kobo ePub features." +#: device/koboextended_config.py:353 +msgid "Hyphenation" +msgstr "" + +#: device/koboextended_config.py:363 +msgid "Select this to add a CSS file which enables hyphenation. The language used will be the language defined for the book in calibre. Please see the README file for directions on updating hyphenation dictionaries." +msgstr "" + +#: device/koboextended_config.py:373 +msgid "Select this to disable hyphenation for books." msgstr "" #: metadata/reader.py:25 @@ -293,3 +244,52 @@ msgstr "" #: metadata/writer.py:36 msgid "Set metadata in Kobo KePub files" msgstr "" + +#: container.py:184 +msgid "A source path must be given" +msgstr "" + +#: container.py:214 +#, python-brace-format +msgid "A valid file name must be given (got {name})" +msgstr "" + +#: container.py:222 +#, python-brace-format +msgid "Could not retrieve content file {infile}" +msgstr "" + +#: container.py:232 +msgid "A section was found but was undefined in content " +msgstr "" + +#: container.py:411 container.py:434 container.py:510 +#, python-brace-format +msgid "Skipping file {name}" +msgstr "" + +#: container.py:414 +msgid "Kobo

tag present" +msgid_plural "Kobo
tags present" +msgstr[0] "" +msgstr[1] "" + +#: container.py:437 +#, python-brace-format +msgid "{div_count}
tag" +msgid_plural "{div_count}
tags" +msgstr[0] "" +msgstr[1] "" + +#: container.py:440 +#, python-brace-format +msgid "{p_count}

tag" +msgid_plural "{p_count}

tags" +msgstr[0] "" +msgstr[1] "" + +#: container.py:513 +msgid "Kobo tag present" +msgid_plural "Kobo tags present" +msgstr[0] "" +msgstr[1] ""