diff --git a/addon/doc/en/readme.md b/addon/doc/en/readme.md index 68671ec..4bcce37 100644 --- a/addon/doc/en/readme.md +++ b/addon/doc/en/readme.md @@ -1,7 +1,7 @@ # nvda-translate Make NVDA translate any spoken text to the desired language. ## Download -- Stable (NVDA 2019.3+): [translate-2021.01](http://www.mtyp.fr/nvda/translate/translate-2021.01.nvda-addon). +- Stable (NVDA 2019.3+): [translate-2021.06.1](http://www.mtyp.fr/nvda/translate/translate-2021.06.1.nvda-addon). ## Installation diff --git a/addon/globalPlugins/translate/__init__.py b/addon/globalPlugins/translate/__init__.py index 1896e84..c29239e 100644 --- a/addon/globalPlugins/translate/__init__.py +++ b/addon/globalPlugins/translate/__init__.py @@ -16,26 +16,22 @@ import os, sys, time, codecs, re import globalVars import globalPluginHandler, logHandler, scriptHandler -try: - import api, controlTypes - import ui, wx, gui - import core, config - import wx - import speech - from speech import * - import json - import queue - curDir = os.path.abspath(os.path.dirname(__file__)) - - sys.path.insert(0, curDir) - sys.path.insert(0, os.path.join(curDir, "html")) - import markupbase - import mtranslate - import updater - import addonHandler, languageHandler -except Exception as e: - logHandler.log.exception("Failed to initialize translate addon", e) - raise e +import api, controlTypes +import ui, wx, gui +import core, config +import wx +import speech +from speech import * +import json +import queue +curDir = os.path.abspath(os.path.dirname(__file__)) +logHandler.log.info("Importing modules from %s" % curDir) +sys.path.insert(0, curDir) +sys.path.insert(0, os.path.join(curDir, "html")) +import markupbase +import mtranslate +import updater +import addonHandler, languageHandler addonHandler.initTranslation() # @@ -88,7 +84,7 @@ def translate(text): # def speak(speechSequence: SpeechSequence, - priority: Optional[Spri] = None): + priority: Spri = None): global _enableTranslation, _lastTranslatedText if _enableTranslation is False: @@ -110,7 +106,7 @@ def speak(speechSequence: SpeechSequence, # def getPropertiesSpeech( # noqa: C901 - reason = controlTypes.REASON_QUERY, + reason = controlTypes.OutputReason.QUERY, **propertyValues ): global oldTreeLevel, oldTableID, oldRowNumber, oldRowSpan, oldColumnNumber, oldColumnSpan @@ -141,9 +137,9 @@ def getPropertiesSpeech( # noqa: C901 and ( roleText or reason not in ( - controlTypes.REASON_SAYALL, - controlTypes.REASON_CARET, - controlTypes.REASON_FOCUS + controlTypes.OutputReason.SAYALL, + controlTypes.OutputReason.CARET, + controlTypes.OutputReason.FOCUS ) or not ( name @@ -157,8 +153,8 @@ def getPropertiesSpeech( # noqa: C901 and ( role != controlTypes.ROLE_MATH or reason not in ( - controlTypes.REASON_CARET, - controlTypes.REASON_SAYALL + controlTypes.OutputReason.CARET, + controlTypes.OutputReason.SAYALL ) )): textList.append(translate(roleText) if roleText else controlTypes.roleLabels[role]) diff --git a/addon/globalPlugins/translate/html/__init__.py b/addon/globalPlugins/translate/customhtml/__init__.py similarity index 100% rename from addon/globalPlugins/translate/html/__init__.py rename to addon/globalPlugins/translate/customhtml/__init__.py diff --git a/addon/globalPlugins/translate/html/entities.py b/addon/globalPlugins/translate/customhtml/entities.py similarity index 100% rename from addon/globalPlugins/translate/html/entities.py rename to addon/globalPlugins/translate/customhtml/entities.py diff --git a/addon/globalPlugins/translate/html/parser.py b/addon/globalPlugins/translate/customhtml/parser.py similarity index 100% rename from addon/globalPlugins/translate/html/parser.py rename to addon/globalPlugins/translate/customhtml/parser.py diff --git a/addon/globalPlugins/translate/mtranslate/core.py b/addon/globalPlugins/translate/mtranslate/core.py index 3200659..96d84a8 100644 --- a/addon/globalPlugins/translate/mtranslate/core.py +++ b/addon/globalPlugins/translate/mtranslate/core.py @@ -33,7 +33,7 @@ import urllib import HTMLParser else: - import html.parser + import customhtml.parser import urllib.request import urllib.parse @@ -53,7 +53,7 @@ def unescape(text): if (sys.version_info[0] < 3): parser = HTMLParser.HTMLParser() else: - parser = html.parser.HTMLParser() + parser = customhtml.parser.HTMLParser() return (parser.unescape(text)) diff --git a/buildVars.py b/buildVars.py index 5bcdf56..4e40853 100644 --- a/buildVars.py +++ b/buildVars.py @@ -1,46 +1,59 @@ -# -*- coding: utf-8 -*- +# -*- coding: UTF-8 -*- # Build customizations # Change this file instead of sconstruct or manifest files, whenever possible. -# Full getext (please don't change) -_ = lambda x : x + +# Since some strings in `addon_info` are translatable, +# we need to include them in the .po files. +# Gettext recognizes only strings given as parameters to the `_` function. +# To avoid initializing translations in this module we simply roll our own "fake" `_` function +# which returns whatever is given to it as an argument. +def _(arg): + return arg + # Add-on information variables addon_info = { - # for previously unpublished addons, please follow the community guidelines at: - # https://bitbucket.org/nvdaaddonteam/todo/raw/master/guideLines.txt - # add-on Name, internal for nvda - "addon_name" : "translate", + # add-on Name/identifier, internal for NVDA + "addon_name": "translate", # Add-on summary, usually the user visible name of the addon. - # Translators: Summary for this add-on to be shown on installation and add-on information. - "addon_summary" : _("Translate"), + # Translators: Summary for this add-on + # to be shown on installation and add-on information found in Add-ons Manager. + "addon_summary": _("Translate"), # Add-on description # Translators: Long description to be shown for this add-on on add-on information from add-ons manager - "addon_description" : _("""Uses the Google Translate API to translate each spoken text to the desired language, on the fly. + "addon_description": _("""Uses the Google Translate API to translate each spoken text to the desired language, on the fly. This add-on requires an internet connection."""), # version - "addon_version" : "2021.01.3", + "addon_version": "2021.06.1", # Author(s) - "addon_author" : u"Yannick PLASSIARD , Hxebolax", + "addon_author": u"Yannick PLASSIARD , Hxebolax", # URL for the add-on documentation support - "addon_url" : None, + "addon_url": None, # Documentation file name - "addon_docFileName" : "readme.html", - # Minimum NVDA version supported (e.g. "2018.3") - "addon_minimumNVDAVersion" : "2019.3", - # Last NVDA version supported/tested (e.g. "2018.4", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion" : "2020.3", - # Add-on update channel (default is stable or None) - "addon_updateChannel" : "stable", + "addon_docFileName": "readme.html", + # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) + "addon_minimumNVDAVersion": "2021.1", + # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) + "addon_lastTestedNVDAVersion": "2021.1", + # Add-on update channel (default is None, denoting stable releases, + # and for development releases, use "dev".) + # Do not change unless you know what you are doing! + "addon_updateChannel": None, } - -import os.path - # Define the python files that are the sources of your add-on. -# You can use glob expressions here, they will be expanded. -pythonSources = ['addon/globalPlugins/translate/*.py', 'addon/globalPlugins/translate/*/*.py'] +# You can either list every file (using ""/") as a path separator, +# or use glob expressions. +# For example to include all files with a ".py" extension from the "globalPlugins" dir of your add-on +# the list can be written as follows: +pythonSources = ["addon/globalPlugins/*.py", + "addon/globalPlugins/*/*.py", + "addon/globalPlugins/*/*/*.py"] +# For more information on SCons Glob expressions please take a look at: +# https://scons.org/doc/production/HTML/scons-user/apd.html + # Files that contain strings for translation. Usually your python sources i18nSources = pythonSources + ["buildVars.py"] @@ -48,3 +61,15 @@ # Files that will be ignored when building the nvda-addon file # Paths are relative to the addon directory, not to the root directory of your addon sources. excludedFiles = [] + +# Base language for the NVDA add-on +# If your add-on is written in a language other than english, modify this variable. +# For example, set baseLanguage to "es" if your add-on is primarily written in spanish. +baseLanguage = "en" + +# Markdown extensions for add-on documentation +# Most add-ons do not require additional Markdown extensions. +# If you need to add support for markup such as tables, fill out the below list. +# Extensions string must be of the form "markdown.extensions.extensionName" +# e.g. "markdown.extensions.tables" to add tables. +markdownExtensions = [] diff --git a/sconstruct b/sconstruct index f126471..ab61654 100644 --- a/sconstruct +++ b/sconstruct @@ -1,62 +1,79 @@ -# NVDA add-on template SCONSTRUCT file -#Copyright (C) 2012, 2014 Rui Batista -#This file is covered by the GNU General Public License. -#See the file COPYING.txt for more details. +# NVDA add-on template SCONSTRUCT file +# Copyright (C) 2012-2021 Rui Batista, Noelia Martinez, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING.txt for more details. import codecs import gettext -import os, sys +import os import os.path import zipfile +import sys -import buildVars +# While names imported below are available by default in every SConscript +# Linters aren't aware about them. +# To avoid Flake8 F821 warnings about them they are imported explicitly. +# When using other Scons functions please add them to the line below. +from SCons.Script import BoolVariable, Builder, Copy, Environment, Variables + +sys.dont_write_bytecode = True + +# Bytecode should not be written for build vars module to keep the repository root folder clean. +import buildVars # NOQA: E402 def md2html(source, dest): import markdown - + # Use extensions if defined. + mdExtensions = buildVars.markdownExtensions lang = os.path.basename(os.path.dirname(source)).replace('_', '-') - title="{addonSummary} {addonVersion}".format(addonSummary=buildVars.addon_info["addon_summary"], addonVersion=buildVars.addon_info["addon_version"]) + localeLang = os.path.basename(os.path.dirname(source)) + try: + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext + summary = _(buildVars.addon_info["addon_summary"]) + except Exception: + summary = buildVars.addon_info["addon_summary"] + title = "{addonSummary} {addonVersion}".format( + addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"] + ) headerDic = { "[[!meta title=\"": "# ", "\"]]": " #", } - # python3 fixes with codecs.open(source, "r", "utf-8") as f: mdText = f.read() - if sys.version_info.major == 2: - for k, v in headerDic.iteritems(): - mdText = mdText.replace(k, v, 1) - else: - for k, v in headerDic.items(): - mdText = mdText.replace(k, v, 1) - - htmlText = markdown.markdown(mdText) + for k, v in headerDic.items(): + mdText = mdText.replace(k, v, 1) + htmlText = markdown.markdown(mdText, extensions=mdExtensions) + # Optimization: build resulting HTML text in one go instead of writing parts separately. + docText = "\n".join([ + "", + "" % lang, + "", + "" + "", + "", + "%s" % title, + "\n", + htmlText, + "\n" + ]) with codecs.open(dest, "w", "utf-8") as f: - f.write("\n" + - "\n" + - "\n" % (lang, lang) + - "\n" + - "\n" + - "\n" + - "%s\n" % title + - "\n\n" - ) - f.write(htmlText) - f.write("\n\n") + f.write(docText) + def mdTool(env): - mdAction=env.Action( - lambda target,source,env: md2html(source[0].path, target[0].path), - lambda target,source,env: 'Generating %s'%target[0], + mdAction = env.Action( + lambda target, source, env: md2html(source[0].path, target[0].path), + lambda target, source, env: 'Generating % s' % target[0], ) - mdBuilder=env.Builder( + mdBuilder = env.Builder( action=mdAction, suffix='.html', src_suffix='.md', ) - env['BUILDERS']['markdown']=mdBuilder + env['BUILDERS']['markdown'] = mdBuilder + vars = Variables() vars.Add("version", "The version of this build", buildVars.addon_info["addon_version"]) @@ -77,29 +94,43 @@ elif env["version"] is not None: if "channel" in env and env["channel"] is not None: env["addon_updateChannel"] = env["channel"] +buildVars.addon_info["addon_version"] = env["addon_version"] +buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"] + addonFile = env.File("${addon_name}-${addon_version}.nvda-addon") + def addonGenerator(target, source, env, for_signature): - action = env.Action(lambda target, source, env : createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, - lambda target, source, env : "Generating Addon %s" % target[0]) + action = env.Action( + lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating Addon %s" % target[0] + ) return action + def manifestGenerator(target, source, env, for_signature): - action = env.Action(lambda target, source, env : generateManifest(source[0].abspath, target[0].abspath) and None, - lambda target, source, env : "Generating manifest %s" % target[0]) + action = env.Action( + lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating manifest %s" % target[0] + ) return action + def translatedManifestGenerator(target, source, env, for_signature): dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), "..")) lang = os.path.basename(dir) - action = env.Action(lambda target, source, env : generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None, - lambda target, source, env : "Generating translated manifest %s" % target[0]) + action = env.Action( + lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None, + lambda target, source, env: "Generating translated manifest %s" % target[0] + ) return action + env['BUILDERS']['NVDAAddon'] = Builder(generator=addonGenerator) env['BUILDERS']['NVDAManifest'] = Builder(generator=manifestGenerator) env['BUILDERS']['NVDATranslatedManifest'] = Builder(generator=translatedManifestGenerator) + def createAddonHelp(dir): docsDir = os.path.join(dir, "doc") if os.path.isfile("style.css"): @@ -107,10 +138,11 @@ def createAddonHelp(dir): cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE")) env.Depends(addon, cssTarget) if os.path.isfile("readme.md"): - readmePath = os.path.join(docsDir, "en", "readme.md") + readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md") readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE")) env.Depends(addon, readmeTarget) + def createAddonBundleFromPath(path, dest): """ Creates a bundle from a directory that contains an addon manifest file.""" basedir = os.path.abspath(path) @@ -121,25 +153,22 @@ def createAddonBundleFromPath(path, dest): for filename in filenames: pathInBundle = os.path.join(relativePath, filename) absPath = os.path.join(dir, filename) - if pathInBundle not in buildVars.excludedFiles: z.write(absPath, pathInBundle) + if pathInBundle not in buildVars.excludedFiles: + z.write(absPath, pathInBundle) return dest + def generateManifest(source, dest): addon_info = buildVars.addon_info - addon_info["addon_version"] = env["addon_version"] - addon_info["addon_updateChannel"] = env["addon_updateChannel"] with codecs.open(source, "r", "utf-8") as f: manifest_template = f.read() manifest = manifest_template.format(**addon_info) with codecs.open(dest, "w", "utf-8") as f: f.write(manifest) + def generateTranslatedManifest(source, language, out): - trans = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]) - if sys.version_info.major == 3: - _ = trans.gettext - else: - _ = trans.ugettext + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext vars = {} for var in ("addon_summary", "addon_description"): vars[var] = _(buildVars.addon_info[var]) @@ -149,19 +178,24 @@ def generateTranslatedManifest(source, language, out): with codecs.open(out, "w", "utf-8") as f: f.write(result) + def expandGlobs(files): return [f for pattern in files for f in env.Glob(pattern)] + addon = env.NVDAAddon(addonFile, env.Dir('addon')) langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))] -#Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated +# Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated for dir in langDirs: poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po")) - moFile=env.gettextMoFile(poFile) + moFile = env.gettextMoFile(poFile) env.Depends(moFile, poFile) - translatedManifest = env.NVDATranslatedManifest(dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")]) + translatedManifest = env.NVDATranslatedManifest( + dir.File("manifest.ini"), + [moFile, os.path.join("manifest-translated.ini.tpl")] + ) env.Depends(translatedManifest, ["buildVars.py"]) env.Depends(addon, [translatedManifest, moFile]) @@ -169,8 +203,9 @@ pythonFiles = expandGlobs(buildVars.pythonSources) for file in pythonFiles: env.Depends(addon, file) -#Convert markdown files to html -createAddonHelp("addon") # We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager +# Convert markdown files to html +# We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager +createAddonHelp("addon") for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')): htmlFile = env.markdown(mdFile) env.Depends(htmlFile, mdFile) @@ -178,11 +213,11 @@ for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')): # Pot target i18nFiles = expandGlobs(buildVars.i18nSources) -gettextvars={ - 'gettext_package_bugs_address' : 'nvda-translations@freelists.org', - 'gettext_package_name' : buildVars.addon_info['addon_name'], - 'gettext_package_version' : buildVars.addon_info['addon_version'] - } +gettextvars = { + 'gettext_package_bugs_address': 'nvda-translations@groups.io', + 'gettext_package_name': buildVars.addon_info['addon_name'], + 'gettext_package_version': buildVars.addon_info['addon_version'] +} pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars) env.Alias('pot', pot) @@ -198,3 +233,4 @@ env.Depends(manifest, "buildVars.py") env.Depends(addon, manifest) env.Default(addon) +env.Clean(addon, ['.sconsign.dblite', 'addon/doc/' + buildVars.baseLanguage + '/']) diff --git a/site_scons/site_tools/gettexttool/__init__.py b/site_scons/site_tools/gettexttool/__init__.py index fa3a937..db9a009 100644 --- a/site_scons/site_tools/gettexttool/__init__.py +++ b/site_scons/site_tools/gettexttool/__init__.py @@ -17,33 +17,39 @@ """ from SCons.Action import Action + def exists(env): return True + XGETTEXT_COMMON_ARGS = ( "--msgid-bugs-address='$gettext_package_bugs_address' " "--package-name='$gettext_package_name' " "--package-version='$gettext_package_version' " + "--keyword=pgettext:1c,2 " "-c -o $TARGET $SOURCES" ) + def generate(env): env.SetDefault(gettext_package_bugs_address="example@example.com") env.SetDefault(gettext_package_name="") env.SetDefault(gettext_package_version="") - env['BUILDERS']['gettextMoFile']=env.Builder( + env['BUILDERS']['gettextMoFile'] = env.Builder( action=Action("msgfmt -o $TARGET $SOURCE", "Compiling translation $SOURCE"), suffix=".mo", src_suffix=".po" ) - env['BUILDERS']['gettextPotFile']=env.Builder( + env['BUILDERS']['gettextPotFile'] = env.Builder( action=Action("xgettext " + XGETTEXT_COMMON_ARGS, "Generating pot file $TARGET"), suffix=".pot") - env['BUILDERS']['gettextMergePotFile']=env.Builder( - action=Action("xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, - "Generating pot file $TARGET"), - suffix=".pot") - + env['BUILDERS']['gettextMergePotFile'] = env.Builder( + action=Action( + "xgettext " + "--omit-header --no-location " + XGETTEXT_COMMON_ARGS, + "Generating pot file $TARGET" + ), + suffix=".pot" + )