Skip to content

Commit b2148e5

Browse files
authored
Allow overriding language for manuscripts (#2593)
2 parents d894375 + 8b86f90 commit b2148e5

File tree

12 files changed

+107
-45
lines changed

12 files changed

+107
-45
lines changed

novelwriter/common.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from urllib.parse import urljoin
3838
from urllib.request import pathname2url
3939

40-
from PyQt6.QtCore import QCoreApplication, QMimeData, QUrl
40+
from PyQt6.QtCore import QCoreApplication, QLocale, QMimeData, QUrl
4141
from PyQt6.QtGui import QAction, QDesktopServices, QFont, QFontDatabase, QFontInfo
4242
from PyQt6.QtWidgets import QMenu, QMenuBar, QWidget
4343

@@ -321,6 +321,17 @@ def processDialogSymbols(symbols: str) -> str:
321321
return result
322322

323323

324+
def processLangCode(code: str) -> str:
325+
"""Validate a language code."""
326+
code = code.strip()
327+
return QLocale(code).name().replace("_", "-") if code else ""
328+
329+
330+
def languageName(code: str) -> str:
331+
"""Return the local name of a language."""
332+
return QLocale(code).nativeLanguageName().title()
333+
334+
324335
def elide(text: str, length: int) -> str:
325336
"""Elide a piece of text to a maximum length."""
326337
if len(text) > (cut := max(4, length)):

novelwriter/config.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
from novelwriter.common import (
4545
NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
46-
formatTimeStamp, joinLines, processDialogSymbols, simplified
46+
formatTimeStamp, joinLines, languageName, processDialogSymbols, simplified
4747
)
4848
from novelwriter.constants import nwFiles, nwQuotes, nwUnicode
4949
from novelwriter.enum import nwTheme
@@ -507,11 +507,11 @@ def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
507507
if lngSet == self.LANG_NW:
508508
fPre = "nw_"
509509
fExt = ".qm"
510-
langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()}
510+
langList = {"en_GB": languageName("en_GB")}
511511
elif lngSet == self.LANG_PROJ:
512512
fPre = "project_"
513513
fExt = ".json"
514-
langList = {"en_GB": QLocale("en_GB").nativeLanguageName().title()}
514+
langList = {"en_GB": languageName("en_GB")}
515515
else:
516516
return []
517517

@@ -521,7 +521,7 @@ def listLanguages(self, lngSet: int) -> list[tuple[str, str]]:
521521
continue
522522

523523
qmLang = qmName[len(fPre):-len(fExt)]
524-
qmName = QLocale(qmLang).nativeLanguageName().title()
524+
qmName = languageName(qmLang)
525525
if qmLang and qmName and qmLang != "en_GB":
526526
langList[qmLang] = qmName
527527

novelwriter/core/buildsettings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@
121121
"doc.colorHeadings": (bool, True),
122122
"doc.scaleHeadings": (bool, True),
123123
"doc.boldHeadings": (bool, True),
124+
"doc.metaLanguage": (str, ""),
124125
"html.addStyles": (bool, True),
125126
"html.preserveTabs": (bool, False),
126127
}
@@ -187,6 +188,7 @@
187188
"doc.colorHeadings": QT_TRANSLATE_NOOP("Builds", "Add Colours to Headings"),
188189
"doc.scaleHeadings": QT_TRANSLATE_NOOP("Builds", "Increase Size of Headings"),
189190
"doc.boldHeadings": QT_TRANSLATE_NOOP("Builds", "Bold Headings"),
191+
"doc.metaLanguage": QT_TRANSLATE_NOOP("Builds", "Override Document Language"),
190192

191193
"html": QT_TRANSLATE_NOOP("Builds", "HTML Options"),
192194
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),

novelwriter/core/docbuild.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def _setupBuild(self, bldObj: Tokenizer) -> dict:
220220
textFont.fromString(self._build.getStr("format.textFont"))
221221

222222
bldObj.setTextFont(textFont)
223-
bldObj.setLanguage(self._project.data.language)
223+
bldObj.setLanguage(self._build.getStr("doc.metaLanguage") or self._project.data.language)
224224

225225
bldObj.setPartitionFormat(
226226
self._build.getStr("headings.fmtPart"),

novelwriter/core/spellcheck.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030
from pathlib import Path
3131
from typing import TYPE_CHECKING
3232

33-
from PyQt6.QtCore import QLocale
34-
33+
from novelwriter.common import languageName
3534
from novelwriter.constants import nwFiles
3635
from novelwriter.error import logException
3736

@@ -139,7 +138,7 @@ def listDictionaries(self) -> list[tuple[str, str]]:
139138
try:
140139
import enchant
141140
tags = [x for x, _ in enchant.list_dicts()]
142-
lang = [(x, f"{QLocale(x).nativeLanguageName().title()} [{x}]") for x in set(tags)]
141+
lang = [(x, f"{languageName(x)} [{x}]") for x in set(tags)]
143142
except Exception:
144143
logger.error("Failed to list languages for enchant spell checking")
145144
return sorted(lang, key=lambda x: x[1])

novelwriter/formats/tohtml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def saveDocument(self, path: Path) -> None:
281281
else:
282282
html = []
283283
html.append("<!DOCTYPE html>")
284-
html.append("<html>")
284+
html.append(f"<html lang='{self._dLocale.bcp47Name()}'>")
285285
html.append("<head>")
286286
html.append(f"<title>{self._project.data.name:s}</title>")
287287
html.append("<meta charset='utf-8'>")

novelwriter/gui/statusbar.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@
2828
from datetime import datetime
2929
from time import time
3030

31-
from PyQt6.QtCore import QLocale, pyqtSlot
31+
from PyQt6.QtCore import pyqtSlot
3232
from PyQt6.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
3333

3434
from novelwriter import CONFIG, SHARED
35-
from novelwriter.common import formatTime
35+
from novelwriter.common import formatTime, languageName
3636
from novelwriter.constants import nwConst, nwLabels, nwStats, trStats
3737
from novelwriter.extensions.modified import NClickableLabel
3838
from novelwriter.extensions.statusled import StatusLED
@@ -208,7 +208,7 @@ def setLanguage(self, language: str, provider: str) -> None:
208208
self.langText.setText(self.tr("None"))
209209
self.langText.setToolTip("")
210210
else:
211-
self.langText.setText(QLocale(language).nativeLanguageName().title())
211+
self.langText.setText(languageName(language))
212212
self.langText.setToolTip(f"{language} ({provider})" if provider else language)
213213

214214
@pyqtSlot(bool)

novelwriter/tools/manussettings.py

Lines changed: 50 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
)
3838

3939
from novelwriter import CONFIG, SHARED
40-
from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda
40+
from novelwriter.common import (
41+
describeFont, fontMatcher, languageName, processLangCode, qtAddAction,
42+
qtLambda
43+
)
4144
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwUnicode, trConst
4245
from novelwriter.core.buildsettings import BuildSettings, FilterMode
4346
from novelwriter.enum import nwStandardButton
@@ -51,8 +54,8 @@
5154
from novelwriter.extensions.switch import NSwitch
5255
from novelwriter.extensions.switchbox import NSwitchBox
5356
from novelwriter.types import (
54-
QtAlignCenter, QtAlignLeft, QtHeaderFixed, QtHeaderStretch, QtRoleAccept,
55-
QtRoleApply, QtRoleDestruct, QtUserRole
57+
QtAlignCenter, QtAlignLeft, QtAlignRightMiddle, QtHeaderFixed,
58+
QtHeaderStretch, QtRoleAccept, QtRoleApply, QtRoleDestruct, QtUserRole
5659
)
5760

5861
if TYPE_CHECKING:
@@ -1287,21 +1290,21 @@ def buildForm(self) -> None:
12871290
self.addGroupLabel(title, section)
12881291

12891292
# Header
1290-
self.odtPageHeader = QLineEdit(self)
1291-
self.odtPageHeader.setMinimumWidth(200)
1293+
self.pageHeader = QLineEdit(self)
1294+
self.pageHeader.setMinimumWidth(200)
12921295
self.btnPageHeader = NIconToolButton(self, iSz)
12931296
self.btnPageHeader.clicked.connect(self._resetPageHeader)
12941297
self.addRow(
1295-
self._build.getLabel("doc.pageHeader"), self.odtPageHeader,
1298+
self._build.getLabel("doc.pageHeader"), self.pageHeader,
12961299
button=self.btnPageHeader, stretch=(1, 1)
12971300
)
12981301

1299-
self.odtPageCountOffset = NSpinBox(self)
1300-
self.odtPageCountOffset.setMinimum(0)
1301-
self.odtPageCountOffset.setMaximum(999)
1302-
self.odtPageCountOffset.setSingleStep(1)
1303-
self.odtPageCountOffset.setMinimumWidth(spW)
1304-
self.addRow(self._build.getLabel("doc.pageCountOffset"), self.odtPageCountOffset)
1302+
self.pageCountOffset = NSpinBox(self)
1303+
self.pageCountOffset.setMinimum(0)
1304+
self.pageCountOffset.setMaximum(999)
1305+
self.pageCountOffset.setSingleStep(1)
1306+
self.pageCountOffset.setMinimumWidth(spW)
1307+
self.addRow(self._build.getLabel("doc.pageCountOffset"), self.pageCountOffset)
13051308

13061309
# Headings
13071310
self.colorHeadings = NSwitch(self, height=iPx)
@@ -1312,6 +1315,20 @@ def buildForm(self) -> None:
13121315
self.addRow(self._build.getLabel("doc.scaleHeadings"), self.scaleHeadings)
13131316
self.addRow(self._build.getLabel("doc.boldHeadings"), self.boldHeadings)
13141317

1318+
# Meta Language
1319+
self.lblMetaLanguage = QLabel(self)
1320+
self.lblMetaLanguage.setAlignment(QtAlignRightMiddle)
1321+
self.lblMetaLanguage.setFixedWidth(200)
1322+
1323+
self.metaLanguage = QLineEdit(self)
1324+
self.metaLanguage.setAlignment(QtAlignCenter)
1325+
self.metaLanguage.setFixedWidth(80)
1326+
self.metaLanguage.textChanged.connect(self._refreshMetaLang)
1327+
1328+
self.addRow(
1329+
self._build.getLabel("doc.metaLanguage"), [self.lblMetaLanguage, 8, self.metaLanguage]
1330+
)
1331+
13151332
# HTML Document
13161333
# =============
13171334

@@ -1437,9 +1454,10 @@ def loadContent(self) -> None:
14371454
self.colorHeadings.setChecked(self._build.getBool("doc.colorHeadings"))
14381455
self.scaleHeadings.setChecked(self._build.getBool("doc.scaleHeadings"))
14391456
self.boldHeadings.setChecked(self._build.getBool("doc.boldHeadings"))
1440-
self.odtPageHeader.setText(self._build.getStr("doc.pageHeader"))
1441-
self.odtPageCountOffset.setValue(self._build.getInt("doc.pageCountOffset"))
1442-
self.odtPageHeader.setCursorPosition(0)
1457+
self.metaLanguage.setText(processLangCode(self._build.getStr("doc.metaLanguage")))
1458+
self.pageHeader.setText(self._build.getStr("doc.pageHeader"))
1459+
self.pageCountOffset.setValue(self._build.getInt("doc.pageCountOffset"))
1460+
self.pageHeader.setCursorPosition(0)
14431461

14441462
# HTML Document
14451463
# =============
@@ -1502,11 +1520,16 @@ def saveContent(self) -> None:
15021520
self._build.setValue("format.rightMargin", self.rightMargin.value())
15031521

15041522
# Documents
1523+
metaLanguage = processLangCode(self.metaLanguage.text())
1524+
15051525
self._build.setValue("doc.colorHeadings", self.colorHeadings.isChecked())
15061526
self._build.setValue("doc.scaleHeadings", self.scaleHeadings.isChecked())
15071527
self._build.setValue("doc.boldHeadings", self.boldHeadings.isChecked())
1508-
self._build.setValue("doc.pageHeader", self.odtPageHeader.text())
1509-
self._build.setValue("doc.pageCountOffset", self.odtPageCountOffset.value())
1528+
self._build.setValue("doc.metaLanguage", metaLanguage)
1529+
self._build.setValue("doc.pageHeader", self.pageHeader.text())
1530+
self._build.setValue("doc.pageCountOffset", self.pageCountOffset.value())
1531+
1532+
self.metaLanguage.setText(metaLanguage)
15101533

15111534
# HTML Document
15121535
self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked())
@@ -1603,10 +1626,17 @@ def _pageSizeValueChanged(self) -> None:
16031626
if index >= 0:
16041627
self.pageSize.setCurrentIndex(index)
16051628

1629+
@pyqtSlot()
16061630
def _resetPageHeader(self) -> None:
1607-
"""Reset the ODT header format to default."""
1608-
self.odtPageHeader.setText(nwHeadFmt.DOC_AUTO)
1609-
self.odtPageHeader.setCursorPosition(0)
1631+
"""Reset the document header format to default."""
1632+
self.pageHeader.setText(nwHeadFmt.DOC_AUTO)
1633+
self.pageHeader.setCursorPosition(0)
1634+
1635+
@pyqtSlot()
1636+
def _refreshMetaLang(self) -> None:
1637+
"""Update the meta language helper info."""
1638+
code = self.metaLanguage.text().strip()
1639+
self.lblMetaLanguage.setText(languageName(code))
16101640

16111641
##
16121642
# Internal Functions

tests/reference/mBuildDocBuild_HTML5_Lorem_Ipsum.htm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!DOCTYPE html>
2-
<html>
2+
<html lang='en-GB'>
33
<head>
44
<title>Lorem Ipsum</title>
55
<meta charset='utf-8'>

tests/test_base/test_base_common.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@
3737
fontMatcher, formatFileFilter, formatInt, formatTime, formatTimeStamp,
3838
formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass,
3939
isItemLayout, isItemType, isListInstance, isTitleTag, joinLines,
40-
jsonCombine, jsonEncode, makeFileNameSafe, minmax, numberToRoman,
41-
openExternalPath, processDialogSymbols, readTextFile, simplified,
42-
transferCase, uniqueCompact, utf16CharMap, xmlElement, xmlIndent,
43-
xmlSubElem, yesNo
40+
jsonCombine, jsonEncode, languageName, makeFileNameSafe, minmax,
41+
numberToRoman, openExternalPath, processDialogSymbols, processLangCode,
42+
readTextFile, simplified, transferCase, uniqueCompact, utf16CharMap,
43+
xmlElement, xmlIndent, xmlSubElem, yesNo
4444
)
4545
from novelwriter.enum import nwItemClass
4646

@@ -411,6 +411,21 @@ def testBaseCommon_processDialogSymbols():
411411
assert processDialogSymbols("-\u2013\u2014\u2015") == "\u2013\u2014\u2015"
412412

413413

414+
@pytest.mark.base
415+
def testBaseCommon_processLangCode():
416+
"""Test the processLangCode function."""
417+
assert processLangCode("") == ""
418+
assert processLangCode(" ") == ""
419+
assert processLangCode("en") == "en-US"
420+
assert processLangCode("en_gb") == "en-GB"
421+
422+
423+
@pytest.mark.base
424+
def testBaseCommon_languageName():
425+
"""Test the languageName function."""
426+
assert languageName("en-GB") == "British English"
427+
428+
414429
@pytest.mark.base
415430
def testBaseCommon_elide():
416431
"""Test the elide function."""

0 commit comments

Comments
 (0)