Add support for external translation tools (#968)

* Make some minor translation fixes
* Remove crowdin file and add import function for crowdin zip file
* Rename fallback project lang file to en_GB
* Update all i18n files from crowdin exports
* Fix tests
* Update docs
This commit is contained in:
Veronica Berglyd Olsen
2022-01-18 19:17:20 +01:00
committed by GitHub
parent 537bf921d2
commit 25038e6cbb
29 changed files with 23239 additions and 23300 deletions
+1 -2
View File
@@ -9,8 +9,7 @@ The following contributions are welcome:
* Bugfixes for new or existing bugs. Please also report new bugs in the issue tracker even if you
also provide a fix. It makes it easier to keep track of what has been fixed and when.
* New translations. You can read more about contributing translations
[here](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md).
* Translations via the [crowdin project page](https://crowdin.com/project/novelwriter).
* Improvements to the documentation. Particularly if the documentation is unclear. Please don't
make any larger changes to the documentation without discussing if with the maintainer first.
* Adaptations, installation or packaging features targeting specific operating systems.
+6 -2
View File
@@ -9,6 +9,7 @@
[![release](https://img.shields.io/github/v/release/vkbo/novelwriter)](https://github.com/vkbo/novelWriter/releases)
[![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter)
[![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter)
[![Crowdin](https://badges.crowdin.net/novelwriter/localized.svg)](https://crowdin.com/project/novelwriter)
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/novelwriter.png">
@@ -67,8 +68,11 @@ also be appreciated, but please make an issue or a discussion topic first. Befor
code, please also read the full
[Contributing Guide](https://github.com/vkbo/novelWriter/blob/main/CONTRIBUTING.md).
New translations are always welcome. Please read the additional
[instructions](https://github.com/vkbo/novelWriter/blob/main/i18n/README.md) for further details.
### Translations
New translations are always welcome. This project uses crowdin to maintain translations, and you
can contribute translations at the [crowdin project page](https://crowdin.com/project/novelwriter).
See also the [Translations of novelWriter](https://github.com/vkbo/novelWriter/issues/93).
## Key Features
-5
View File
@@ -1,5 +0,0 @@
files:
- source: /i18n/nw_base.ts
translation: /i18n/nw_%locale_with_underscore%.ts
- source: /novelwriter/assets/i18n/project_en.json
translation: /novelwriter/assets/i18n/project_%locale_with_underscore%.json
-116
View File
@@ -1,116 +0,0 @@
# novelWriter Internationalisation (i18n)
Here you will find instructions for translating novelWriter to a new language or updating the
translations for an already existing supported language.
There are two areas relevant to localisation:
* The Qt5 GUI translation files, which consists of `nw_XX.ts` files. This is the bulk of the
translation work.
* The `project_XX.json` files. See [Project Localisation](#project-localisation) below.
**Important:**
When making a new translation, or updating an existing one, only commit the `nw_XX.ts` (and
`project_XX.json`) files you have made changes to. The `qtlupdate` command mentioned below may
modify all `nw_XX.ts` slightly, but please _don't_ commit those changes to the pull request.
## Qt5 GUI Localisation
You will need the translation tool Qt 5 Linguist on your system.
For Ubuntu/Debian, run:
```bash
sudo apt install qttools5-dev-tools
```
See also [Qt Linguist Manual: Translators](https://doc.qt.io/qt-5/linguist-translators.html).
### Workflow
Here's an overview of the localisation workflow:
1. First, create a new branch off of `main`, and there, generate a new `nw_XX.ts` translation file.
1. Load the translation file in Qt Linguist. Translate and save your changes.
1. Generate the `nw_XX.qm` file.
1. Run the application and verify your changes. Iterate from step 2.
1. Once finished, commit your changes and submit your pull request.
When you want to translate new changes, again create a new branch off of `main` and re-generate the
translation file. Continue from step 2.
### Generate an Updated Translation File
The `i18n` folder at the root of the repository contains the `nw_XX.ts` translation files.
Whether to add a new language to the translation framework, or to update the file against the
current source code, you must first run the `qtlupdate` command:
```bash
python3 setup.py qtlupdate i18n/nw_XX.ts
```
The file name must be in the correct format. All translation files must be located in the `i18n`
folder, start with `nw_` and end with `.ts`. In between goes the language code. It must be a valid
ISO language code, otherwise novelWriter will not accept the file.
For instance, the French translation uses the language code `fr`, so its translation file will be
`nw_fr.ts`
**Note:** If you run the `qtlupdate` command without a file name, it will update translation files
for all languages.
### Edit the Translation File in Qt Linguist
The `i18n/nw_XX.ts` file can be edited with the Qt Linguist application provided by Qt 5. This is
by far easier than manually editing the `.ts` file.
Please select "English" and "United Kingdom" as the _source_ language if prompted by Qt Linguist.
### Verify Your Translation
The application does not use `.ts` files directly. The `novelwriter/assets/i18n/nw_XX.qm` files are
the actual files used to translate the GUI into another language other than the default British
English. These files are not generated by default, but they can be built with:
```bash
python3 setup.py qtlrelease
```
You can now test the translation in novelWriter. The Preferences dialog should list the newly added
language, so go ahead and select it.
**Note:** Please do not submit the `.qm` files to the repository. Only the `.ts` file you just
added in the `i18n` folder is needed.
### Missing QtBase Translations
The default Qt dialogs also have translations, for instance for standard buttons like "Yes", "No",
"Ok", "Cancel", etc. Generally, these translation files are installed with the Qt libraries on your
system, and novelWriter will collect those translations from there. However, these translations are
missing for many languages.
As a starting point, there is no need to translate any entries in the `.ts` files that are under
elements starting with the letter "Q", like "QPlatformTheme", "QWizard", etc. If these turn up in
English in novelWriter after activating a translation, it means they are probably missing in the Qt
library, and you may also need to translate these.
These additional translation entries are generated from a file named `i18n/qtbase.py`, which is not
a file that novelWriter uses. It is there only to generate these additional entries for the `.ts`
files.
## Project Localisation
Projects can have a different language setting than the GUI itself. The files with format
`project_XX.json` in `novelwriter/assets/i18n` are simple translation maps for text that goes into
the exported documents generated by the Build Novel Project tool.
The files are loaded based on the language setting in the Build Novel Project tool. At the present
time, no other parts of the application use these files. Since these are used as replacement
lookups, they are maintained as plain JSON files. The main usage is to generate chapter headers as
number words.
Adding new translations for these files is easy. Just copy the `project_en.json` file, rename it to
the appropriate language coded filename, and open it in a text editor. Please use the underscore as
separator if a language needs to be localised to a specific country under a language. The importer
will for instance look for `en_US` first, then use `en` if it is not found. If there are no
matching files, the project falls back to using English.
+32 -40
View File
@@ -801,7 +801,7 @@
</message>
<message>
<location filename="../novelwriter/tools/build.py" line="1354" />
<source>&lt;b&gt;Build Time:&lt;/b&gt; {0}</source>
<source>Build Time:</source>
<translation type="unfinished" />
</message>
</context>
@@ -984,7 +984,7 @@
</message>
<message>
<location filename="../novelwriter/gui/doceditor.py" line="480" />
<source>This document has been changed outside of novelWriter while it was open. Overvrite the file on disk?</source>
<source>This document has been changed outside of novelWriter while it was open. Overwrite the file on disk?</source>
<translation type="unfinished" />
</message>
<message>
@@ -5029,103 +5029,95 @@
<translation type="unfinished" />
</message>
</context>
<context>
<name>QGuiApplication</name>
<message>
<location filename="qtbase.py" line="32" />
<source>Translate this string to the string 'LTR' in left-to-right languages or to 'RTL' in right-to-left languages (such as Hebrew and Arabic) to get proper widget layout.</source>
<translation type="unfinished" />
</message>
</context>
<context>
<name>QPlatformTheme</name>
<message>
<location filename="qtbase.py" line="40" />
<location filename="qtbase.py" line="32" />
<source>OK</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="41" />
<location filename="qtbase.py" line="33" />
<source>Save</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="42" />
<location filename="qtbase.py" line="34" />
<source>Save All</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="43" />
<location filename="qtbase.py" line="35" />
<source>Open</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="44" />
<location filename="qtbase.py" line="36" />
<source>&amp;Yes</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="45" />
<location filename="qtbase.py" line="37" />
<source>Yes to &amp;All</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="46" />
<location filename="qtbase.py" line="38" />
<source>&amp;No</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="47" />
<location filename="qtbase.py" line="39" />
<source>N&amp;o to All</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="48" />
<location filename="qtbase.py" line="40" />
<source>Abort</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="49" />
<location filename="qtbase.py" line="41" />
<source>Retry</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="50" />
<location filename="qtbase.py" line="42" />
<source>Ignore</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="51" />
<location filename="qtbase.py" line="43" />
<source>Close</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="52" />
<location filename="qtbase.py" line="44" />
<source>Cancel</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="53" />
<location filename="qtbase.py" line="45" />
<source>Discard</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="54" />
<location filename="qtbase.py" line="46" />
<source>Help</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="55" />
<location filename="qtbase.py" line="47" />
<source>Apply</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="56" />
<location filename="qtbase.py" line="48" />
<source>Reset</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="57" />
<location filename="qtbase.py" line="49" />
<source>Restore Defaults</source>
<translation type="unfinished" />
</message>
@@ -5133,58 +5125,58 @@
<context>
<name>QWizard</name>
<message>
<location filename="qtbase.py" line="62" />
<location filename="qtbase.py" line="54" />
<source>Go Back</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="63" />
<location filename="qtbase.py" line="55" />
<source>&lt; &amp;Back</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="64" />
<location filename="qtbase.py" line="56" />
<source>Continue</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="65" />
<location filename="qtbase.py" line="57" />
<source>&amp;Next</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="66" />
<location filename="qtbase.py" line="58" />
<source>&amp;Next &gt;</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="67" />
<location filename="qtbase.py" line="59" />
<source>Commit</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="68" />
<location filename="qtbase.py" line="60" />
<source>Done</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="69" />
<location filename="qtbase.py" line="61" />
<source>&amp;Finish</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="71" />
<location filename="qtbase.py" line="70" />
<location filename="qtbase.py" line="63" />
<location filename="qtbase.py" line="62" />
<source>Cancel</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="72" />
<location filename="qtbase.py" line="64" />
<source>Help</source>
<translation type="unfinished" />
</message>
<message>
<location filename="qtbase.py" line="73" />
<location filename="qtbase.py" line="65" />
<source>&amp;Help</source>
<translation type="unfinished" />
</message>
+3156 -3164
View File
File diff suppressed because it is too large Load Diff
+3156 -3190
View File
File diff suppressed because it is too large Load Diff
-5210
View File
File diff suppressed because it is too large Load Diff
+5202
View File
File diff suppressed because it is too large Load Diff
+3156 -3164
View File
File diff suppressed because it is too large Load Diff
-5210
View File
File diff suppressed because it is too large Load Diff
+5202
View File
File diff suppressed because it is too large Load Diff
+3157 -3165
View File
File diff suppressed because it is too large Load Diff
-8
View File
@@ -26,14 +26,6 @@ QT_TRANSLATE_NOOP("QGnomeTheme", "&Cancel")
QT_TRANSLATE_NOOP("QGnomeTheme", "&Close")
QT_TRANSLATE_NOOP("QGnomeTheme", "Close without Saving")
# QGuiApplication
# ===============
QT_TRANSLATE_NOOP("QGuiApplication", (
"Translate this string to the string 'LTR' in left-to-right languages or to "
"'RTL' in right-to-left languages (such as Hebrew and Arabic) to get proper widget layout."
))
# QPlatformTheme
# ==============
+105
View File
@@ -0,0 +1,105 @@
{
"Synopsis": "Synopsis",
"Comment": "Comment",
"Notes": "Notes",
"0": "Zero",
"1": "One",
"2": "Two",
"3": "Three",
"4": "Four",
"5": "Five",
"6": "Six",
"7": "Seven",
"8": "Eight",
"9": "Nine",
"10": "Ten",
"11": "Eleven",
"12": "Twelve",
"13": "Thirteen",
"14": "Fourteen",
"15": "Fifteen",
"16": "Sixteen",
"17": "Seventeen",
"18": "Eighteen",
"19": "Nineteen",
"20": "Twenty",
"21": "Twenty-One",
"22": "Twenty-Two",
"23": "Twenty-Three",
"24": "Twenty-Four",
"25": "Twenty-Five",
"26": "Twenty-Six",
"27": "Twenty-Seven",
"28": "Twenty-Eight",
"29": "Twenty-Nine",
"30": "Thirty",
"31": "Thirty-One",
"32": "Thirty-Two",
"33": "Thirty-Three",
"34": "Thirty-Four",
"35": "Thirty-Five",
"36": "Thirty-Six",
"37": "Thirty-Seven",
"38": "Thirty-Eight",
"39": "Thirty-Nine",
"40": "Forty",
"41": "Forty-One",
"42": "Forty-Two",
"43": "Forty-Three",
"44": "Forty-Four",
"45": "Forty-Five",
"46": "Forty-Six",
"47": "Forty-Seven",
"48": "Forty-Eight",
"49": "Forty-Nine",
"50": "Fifty",
"51": "Fifty-One",
"52": "Fifty-Two",
"53": "Fifty-Three",
"54": "Fifty-Four",
"55": "Fifty-Five",
"56": "Fifty-Six",
"57": "Fifty-Seven",
"58": "Fifty-Eight",
"59": "Fifty-Nine",
"60": "Sixty",
"61": "Sixty-One",
"62": "Sixty-Two",
"63": "Sixty-Three",
"64": "Sixty-Four",
"65": "Sixty-Five",
"66": "Sixty-Six",
"67": "Sixty-Seven",
"68": "Sixty-Eight",
"69": "Sixty-Nine",
"70": "Seventy",
"71": "Seventy-One",
"72": "Seventy-Two",
"73": "Seventy-Three",
"74": "Seventy-Four",
"75": "Seventy-Five",
"76": "Seventy-Six",
"77": "Seventy-Seven",
"78": "Seventy-Eight",
"79": "Seventy-Nine",
"80": "Eighty",
"81": "Eighty-One",
"82": "Eighty-Two",
"83": "Eighty-Three",
"84": "Eighty-Four",
"85": "Eighty-Five",
"86": "Eighty-Six",
"87": "Eighty-Seven",
"88": "Eighty-Eight",
"89": "Eighty-Nine",
"90": "Ninety",
"91": "Ninety-One",
"92": "Ninety-Two",
"93": "Ninety-Three",
"94": "Ninety-Four",
"95": "Ninety-Five",
"96": "Ninety-Six",
"97": "Ninety-Seven",
"98": "Ninety-Eight",
"99": "Ninety-Nine"
}
+1 -1
View File
@@ -32,7 +32,7 @@
"27": "二十七",
"28": "二十八",
"29": "二十九",
"30": "三十",
"30": "三十",
"31": "三十一",
"32": "三十二",
"33": "三十三",
+2 -2
View File
@@ -396,7 +396,7 @@ class Config:
elif lngSet == self.LANG_PROJ:
fPre = "project_"
fExt = ".json"
langList = {"en": "English"}
langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()}
else:
return []
@@ -407,7 +407,7 @@ class Config:
continue
qmLang = qmFile[len(fPre):-len(fExt)]
qmName = QLocale(qmLang).nativeLanguageName().title()
if qmLang and qmName and qmLang != "en":
if qmLang and qmName and qmLang != "en_GB":
langList[qmLang] = qmName
return sorted(langList.items(), key=lambda x: x[0])
+5 -11
View File
@@ -1242,20 +1242,14 @@ class NWProject():
self.langData = {}
return False
lngShort = self.projLang.split("_")[0]
loadFile = os.path.join(self.mainConf.nwLangPath, "project_en.json")
chkFile1 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
chkFile2 = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % lngShort)
if os.path.isfile(chkFile1):
loadFile = chkFile1
elif os.path.isfile(chkFile2):
loadFile = chkFile2
langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
if not os.path.isfile(langFile):
langFile = os.path.join(self.mainConf.nwLangPath, "project_en_GB.json")
try:
with open(loadFile, mode="r", encoding="utf-8") as inFile:
with open(langFile, mode="r", encoding="utf-8") as inFile:
self.langData = json.load(inFile)
logger.debug("Loaded project language file: %s", os.path.basename(loadFile))
logger.debug("Loaded project language file: %s", os.path.basename(langFile))
except Exception:
logger.error("Failed to project language file")
+1 -1
View File
@@ -479,7 +479,7 @@ class GuiDocEditor(QTextEdit):
self.tr("File Changed on Disk"),
self.tr(
"This document has been changed outside of novelWriter "
"while it was open. Overvrite the file on disk?"
"while it was open. Overwrite the file on disk?"
)
)
if msgYes:
+1 -1
View File
@@ -1351,7 +1351,7 @@ class GuiBuildNovelDocView(QTextBrowser):
else:
strBuildTime = self.tr("Unknown")
self.theTitle.setText(self.tr("<b>Build Time:</b> {0}").format(strBuildTime))
self.theTitle.setText("%s %s" % (self.tr("Build Time:"), strBuildTime))
return
+5 -5
View File
@@ -1,17 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.6-beta1" hexVersion="0x010600b1" fileVersion="1.3" timeStamp="2022-01-18 09:44:14">
<novelWriterXML appVersion="1.6-beta1" hexVersion="0x010600b1" fileVersion="1.3" timeStamp="2022-01-18 18:48:36">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1276</saveCount>
<autoCount>198</autoCount>
<editTime>63357</editTime>
<saveCount>1283</saveCount>
<autoCount>199</autoCount>
<editTime>63909</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<language>en</language>
<language>en_GB</language>
<spellCheck>True</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
+48
View File
@@ -27,6 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import sys
import shutil
import zipfile
import datetime
import subprocess
import email.utils
@@ -435,6 +436,47 @@ def buildSampleZip():
# Python Packaging
# =============================================================================================== #
##
# Import Translations (import-i18n)
##
def importI18nUpdates(sysArgs):
"""Import new translation files from a zip file.
"""
print("")
print("Import Updated Translations")
print("===========================")
print("")
fileName = None
if len(sysArgs) >= 2:
if os.path.isfile(sysArgs[1]):
fileName = sysArgs[1]
if fileName is None:
print("File not found ...")
sys.exit(1)
projPath = os.path.join("novelwriter", "assets", "i18n")
mainPath = "i18n"
print("Loading file: %s" % fileName)
with zipfile.ZipFile(fileName) as zipObj:
for archFile in zipObj.namelist():
if archFile.startswith("nw_") and archFile.endswith(".ts"):
zipObj.extract(archFile, mainPath)
print("Extracted: %s > %s" % (archFile, os.path.join(mainPath, archFile)))
elif archFile.startswith("project_") and archFile.endswith(".json"):
zipObj.extract(archFile, projPath)
print("Extracted: %s > %s" % (archFile, os.path.join(projPath, archFile)))
else:
print("Skipped: %s" % archFile)
print("")
return
##
# Make Minimal Package (minimal-zip)
##
@@ -1514,6 +1556,7 @@ if __name__ == "__main__":
"",
"Python Packaging:",
"",
" import-i18n Import updated i18n files from a zip file.",
" minimal-zip Creates a minimal zip file of the core application without",
" all the other source files. Defaults to tailor the zip file",
" for the current OS, but accepts a target OS flag to build",
@@ -1597,6 +1640,11 @@ if __name__ == "__main__":
# Python Packaging
# ================
if "import-i18n" in sys.argv:
sys.argv.remove("import-i18n")
importI18nUpdates(sys.argv)
sys.exit(0) # Don't continue execution
if "minimal-zip" in sys.argv:
sys.argv.remove("minimal-zip")
makeMinimalPackage(targetOS)
+1 -1
View File
@@ -10,7 +10,7 @@
</project>
<settings>
<doBackup>False</doBackup>
<language>en</language>
<language>en_GB</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
+1 -1
View File
@@ -11,7 +11,7 @@
</project>
<settings>
<doBackup>True</doBackup>
<language>en</language>
<language>en_GB</language>
<spellCheck>False</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
+1 -1
View File
@@ -243,7 +243,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir):
theList = tstConf.listLanguages(tstConf.LANG_NW)
assert theList == [("en_GB", "British English")]
theList = tstConf.listLanguages(tstConf.LANG_PROJ)
assert theList == [("en", "English")]
assert theList == [("en_GB", "British English")]
theList = tstConf.listLanguages(None)
assert theList == []