Merge pull request #676 from vkbo/tokenizer_localisation

Tokenizer Localisation
This commit is contained in:
Veronica Berglyd Olsen
2021-02-17 01:37:58 +01:00
committed by GitHub
15 changed files with 577 additions and 441 deletions
+183 -203
View File
File diff suppressed because it is too large Load Diff
+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"
}
+105
View File
@@ -0,0 +1,105 @@
{
"Synopsis": "Sammendrag",
"Comment": "Kommentar",
"Notes": "Notat",
"0": "null",
"1": "én",
"2": "to",
"3": "tre",
"4": "fire",
"5": "fem",
"6": "seks",
"7": "sju",
"8": "åtte",
"9": "ni",
"10": "ti",
"11": "elleve",
"12": "tolv",
"13": "tretten",
"14": "fjorten",
"15": "femten",
"16": "seksten",
"17": "sytten",
"18": "atten",
"19": "nitten",
"20": "tjue",
"21": "tjueén",
"22": "tjueto",
"23": "tjuetre",
"24": "tjuefire",
"25": "tjuefem",
"26": "tjueseks",
"27": "tjuesju",
"28": "tjueåtte",
"29": "tjueni",
"30": "tretti",
"31": "trettién",
"32": "trettito",
"33": "trettitre",
"34": "trettifire",
"35": "trettifem",
"36": "trettiseks",
"37": "trettisju",
"38": "trettiåtte",
"39": "trettini",
"40": "førti",
"41": "førtién",
"42": "førtito",
"43": "førtitre",
"44": "førtifire",
"45": "førtifem",
"46": "førtiseks",
"47": "førtisju",
"48": "førtiåtte",
"49": "førtini",
"50": "femti",
"51": "femtién",
"52": "femtito",
"53": "femtitre",
"54": "femtifire",
"55": "femtifem",
"56": "femtiseks",
"57": "femtisju",
"58": "femtiåtte",
"59": "femtini",
"60": "seksti",
"61": "sekstién",
"62": "sekstito",
"63": "sekstitre",
"64": "sekstifire",
"65": "sekstifem",
"66": "sekstiseks",
"67": "sekstisju",
"68": "sekstiåtte",
"69": "sekstini",
"70": "sytti",
"71": "syttién",
"72": "syttito",
"73": "syttitre",
"74": "syttifire",
"75": "syttifem",
"76": "syttiseks",
"77": "syttisju",
"78": "syttiåtte",
"79": "syttini",
"80": "åtti",
"81": "åttién",
"82": "åttito",
"83": "åttitre",
"84": "åttifire",
"85": "åttifem",
"86": "åttiseks",
"87": "åttisju",
"88": "åttiåtte",
"89": "åttini",
"90": "nitti",
"91": "nittién",
"92": "nittito",
"93": "nittitre",
"94": "nittifire",
"95": "nittifem",
"96": "nittiseks",
"97": "nittisju",
"98": "nittiåtte",
"99": "nittini"
}
+105
View File
@@ -0,0 +1,105 @@
{
"Synopsis": "Samandrag",
"Comment": "Kommentar",
"Notes": "Notat",
"0": "null",
"1": "ein",
"2": "to",
"3": "tre",
"4": "fire",
"5": "fem",
"6": "seks",
"7": "sju",
"8": "åtte",
"9": "ni",
"10": "ti",
"11": "elleve",
"12": "tolv",
"13": "tretten",
"14": "fjorten",
"15": "femten",
"16": "seksten",
"17": "sytten",
"18": "atten",
"19": "nitten",
"20": "tjue",
"21": "tjueein",
"22": "tjueto",
"23": "tjuetre",
"24": "tjuefire",
"25": "tjuefem",
"26": "tjueseks",
"27": "tjuesju",
"28": "tjueåtte",
"29": "tjueni",
"30": "tretti",
"31": "trettiein",
"32": "trettito",
"33": "trettitre",
"34": "trettifire",
"35": "trettifem",
"36": "trettiseks",
"37": "trettisju",
"38": "trettiåtte",
"39": "trettini",
"40": "førti",
"41": "førtiein",
"42": "førtito",
"43": "førtitre",
"44": "førtifire",
"45": "førtifem",
"46": "førtiseks",
"47": "førtisju",
"48": "førtiåtte",
"49": "førtini",
"50": "femti",
"51": "femtiein",
"52": "femtito",
"53": "femtitre",
"54": "femtifire",
"55": "femtifem",
"56": "femtiseks",
"57": "femtisju",
"58": "femtiåtte",
"59": "femtini",
"60": "seksti",
"61": "sekstiein",
"62": "sekstito",
"63": "sekstitre",
"64": "sekstifire",
"65": "sekstifem",
"66": "sekstiseks",
"67": "sekstisju",
"68": "sekstiåtte",
"69": "sekstini",
"70": "sytti",
"71": "syttiein",
"72": "syttito",
"73": "syttitre",
"74": "syttifire",
"75": "syttifem",
"76": "syttiseks",
"77": "syttisju",
"78": "syttiåtte",
"79": "syttini",
"80": "åtti",
"81": "åttiein",
"82": "åttito",
"83": "åttitre",
"84": "åttifire",
"85": "åttifem",
"86": "åttiseks",
"87": "åttisju",
"88": "åttiåtte",
"89": "åttini",
"90": "nitti",
"91": "nittiein",
"92": "nittito",
"93": "nittitre",
"94": "nittifire",
"95": "nittifem",
"96": "nittiseks",
"97": "nittisju",
"98": "nittiåtte",
"99": "nittini"
}
+2
View File
@@ -76,6 +76,7 @@ class Config:
self.themeRoot = None # The full path to the nw/assets/themes folder
self.dictPath = None # The full path to the nw/assets/dict folder
self.iconPath = None # The full path to the nw/assets/icons folder
self.langPath = None # The full path to the nw/assets/lang folder
self.helpPath = None # The full path to the novelwriter .qhc help file
# Runtime Settings and Variables
@@ -297,6 +298,7 @@ class Config:
self.themeRoot = os.path.join(self.assetPath, "themes")
self.dictPath = os.path.join(self.assetPath, "dict")
self.iconPath = os.path.join(self.assetPath, "icons")
self.langPath = os.path.join(self.assetPath, "lang")
self.appIcon = os.path.join(self.iconPath, "novelwriter.svg")
# Internationalisation
+1 -2
View File
@@ -7,12 +7,11 @@ from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.core.tohtml import ToHtml
from nw.core.toodt import ToOdt
from nw.core.tomd import ToMarkdown
from nw.core.tools import countWords, numberToRoman, numberToWord
from nw.core.tools import countWords, numberToRoman
__all__ = [
"countWords",
"numberToRoman",
"numberToWord",
"NWDoc",
"NWIndex",
"NWProject",
+42
View File
@@ -27,6 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import nw
import logging
import os
import json
import shutil
from lxml import etree
@@ -61,6 +62,7 @@ class NWProject():
# Core Elements
self.optState = OptionState(self) # Project-specific GUI options
self.projTree = NWTree(self) # The project tree
self.langData = {} # Localisation data
# Project Status
self.projOpened = 0 # The time stamp of when the project file was opened
@@ -622,6 +624,7 @@ class NWProject():
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
self._scanProjectFolder()
self.loadProjectLocalisation(self.projLang)
self.currWCount = self.lastWCount
self.projOpened = time()
@@ -1046,6 +1049,7 @@ class NWProject():
theLang = checkString(theLang, None, True)
if self.projLang != theLang:
self.projLang = theLang
self.loadProjectLocalisation(theLang)
self.setProjectChanged(True)
return True
@@ -1233,6 +1237,44 @@ class NWProject():
self.importItems.countEntry(nwItem.itemStatus)
return
def localLookup(self, theWord):
"""Look up a word in the translation map for the project and
return it. The variable is cast to a string before lookup. If
the word does not exist, it returns itself.
"""
theValue = str(theWord)
return self.langData.get(theValue, theValue)
def loadProjectLocalisation(self, theLang):
"""Load the language data for the current project language.
"""
if theLang is None:
theLang = self.mainConf.spellLanguage
if theLang is None:
theLang = "en"
lngShort = theLang.split("_")[0]
loadFile = os.path.join(self.mainConf.langPath, "project_en.json")
chkFile1 = os.path.join(self.mainConf.langPath, "project_%s.json" % theLang)
chkFile2 = os.path.join(self.mainConf.langPath, "project_%s.json" % lngShort)
if os.path.isfile(chkFile1):
loadFile = chkFile1
elif os.path.isfile(chkFile2):
loadFile = chkFile2
try:
with open(loadFile, mode="r", encoding="utf8") as inFile:
self.langData = json.load(inFile)
logger.debug("Loaded project language file: %s" % os.path.basename(loadFile))
except Exception:
logger.error("Failed to load index file")
nw.logException()
return False
return True
##
# Internal Functions
##
+6 -10
View File
@@ -405,24 +405,20 @@ class ToHtml(Tokenizer):
def _formatSynopsis(self, tText):
"""Apply HTML formatting to synopsis.
"""
sSynop = self._localLookup("Synopsis")
if self.genMode == self.M_PREVIEW:
return "<p class='comment'><span class='synopsis'>%s:</span> %s</p>\n" % (
self._trSynopsis, tText
)
return f"<p class='comment'><span class='synopsis'>{sSynop}:</span> {tText}</p>\n"
else:
return "<p class='synopsis'><strong>%s:</strong> %s</p>\n" % (
self._trSynopsis, tText
)
return f"<p class='synopsis'><strong>{sSynop}:</strong> {tText}</p>\n"
def _formatComments(self, tText):
"""Apply HTML formatting to comments.
"""
if self.genMode == self.M_PREVIEW:
return "<p class='comment'>%s</p>\n" % tText
return f"<p class='comment'>{tText}</p>\n"
else:
return "<p class='comment'><strong>%s:</strong> %s</p>\n" % (
self._trComment, tText
)
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {tText}</p>\n"
def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords.
+7 -14
View File
@@ -33,7 +33,7 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord, numberToRoman
from nw.core.tools import numberToRoman
from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx
logger = logging.getLogger(__name__)
@@ -143,18 +143,10 @@ class Tokenizer():
# Error Handling
self.errData = []
# Internal Mappings
# Function Mapping
self._localLookup = self.theProject.localLookup
self.tr = partial(QCoreApplication.translate, "Tokenizer")
# Localisation
self._trSynopsis = self.tr("Synopsis")
self._trComment = self.tr("Comment")
self._trNotes = self.tr("Notes")
self._spellLang = self.theProject.projLang
if self._spellLang is None:
self._spellLang = self.theParent.mainConf.spellLanguage
return
##
@@ -263,7 +255,7 @@ class Tokenizer():
if theItem.itemType != nwItemType.ROOT:
return False
theTitle = self.tr("{0}: {1}").format(self._trNotes, theItem.itemName)
theTitle = "%s: %s" % (self._localLookup("Notes"), theItem.itemName)
self.theTokens = []
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
@@ -667,11 +659,12 @@ class Tokenizer():
theTitle = theTitle.replace(r"%sc%", str(self.numChScene))
theTitle = theTitle.replace(r"%sca%", str(self.numAbsScene))
if r"%chw%" in theTitle:
theTitle = theTitle.replace(r"%chw%", numberToWord(self.numChapter, self._spellLang))
theTitle = theTitle.replace(r"%chw%", self._localLookup(self.numChapter))
if r"%chi%" in theTitle:
theTitle = theTitle.replace(r"%chi%", numberToRoman(self.numChapter, True))
if r"%chI%" in theTitle:
theTitle = theTitle.replace(r"%chI%", numberToRoman(self.numChapter, False))
return theTitle
return theTitle[:1].upper() + theTitle[1:]
# END Class Tokenizer
+2 -2
View File
@@ -134,10 +134,10 @@ class ToMarkdown(Tokenizer):
thisPar.append(tTemp.rstrip() + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**Synopsis:** %s\n\n" % tText)
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Synopsis"), tText))
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append("**Comment:** %s\n\n" % tText)
tmpResult.append("**%s:** %s\n\n" % (self._localLookup("Comment"), tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
+7 -5
View File
@@ -507,15 +507,17 @@ class ToOdt(Tokenizer):
def _formatSynopsis(self, tText):
"""Apply formatting to synopsis lines.
"""
rTxt = "**Synopsis:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
sSynop = self._localLookup("Synopsis")
rTxt = "**%s:** %s" % (sSynop, tText)
rFmt = "_B%s b_ %s" % (" "*len(sSynop), " "*len(tText))
return rTxt, rFmt
def _formatComments(self, tText):
"""Apply formatting to comments.
"""
rTxt = "**Comment:** %s" % tText
rFmt = "_B b_ %s" % (" "*len(tText))
sComm = self._localLookup("Comment")
rTxt = "**%s:** %s" % (sComm, tText)
rFmt = "_B%s b_ %s" % (" "*len(sComm), " "*len(tText))
return rTxt, rFmt
def _formatKeywords(self, tText):
@@ -530,7 +532,7 @@ class ToOdt(Tokenizer):
if theBits[0] in nwLabels.KEY_NAME:
tText = nwLabels.KEY_NAME[theBits[0]]
rTxt += "**%s:** " % tText
rFmt += "_B%s b_ " % (" "*len(tText))
rFmt += "_B%s b_ " % (" "*len(tText))
if len(theBits) > 1:
if theBits[0] == nwKeyWords.TAG_KEY:
rTxt += "%s" % theBits[1]
-125
View File
@@ -6,8 +6,6 @@ Various core tool functions
File History:
Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Merged: 2020-05-08 [0.4.5] All of the above into this file
Created: 2020-07-05 [0.10.0] numberToRoman
This file is a part of novelWriter
@@ -118,126 +116,3 @@ def numberToRoman(numVal, isLower=False):
break
return romNum.lower() if isLower else romNum
# =============================================================================================== #
# Convert an Integer to a Number String
# =============================================================================================== #
def numberToWord(numVal, theLang):
"""Wrapper for converting numbers to words for chapter headings.
"""
if not isinstance(numVal, int):
return "[NaN]"
if numVal < 0:
return "[<0]"
if numVal > 999:
return "[>999]"
numWord = ""
theCode = theLang.split("_")[0]
if theCode == "en":
numWord = _numberToWord_EN(numVal)
elif theCode in ("nb", "nn"):
numWord = _numberToWord_NO(numVal, theCode)
else:
# Default to return the number as a string
numWord = str(numVal)
return numWord
def _numberToWord_EN(numVal):
"""Convert numbers to English words.
"""
oneWord = ""
tenWord = ""
hunWord = ""
if numVal == 0:
return "Zero"
oneVal = numVal % 10
tenVal = (numVal - oneVal) % 100
hunVal = (numVal - tenVal - oneVal) % 1000
theWords = {
0: "", 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", 30: "Thirty", 40: "Forty",
50: "Fifty", 60: "Sixty", 70: "Seventy", 80: "Eighty",
90: "Ninety", 100: "One Hundred", 200: "Two Hundred",
300: "Three Hundred", 400: "Four Hundred", 500: "Five Hundred",
600: "Six Hundred", 700: "Seven Hundred", 800: "Eight Hundred",
900: "Nine Hundred",
}
retVal = ""
hunWord = theWords.get(hunVal, "")
if tenVal == 10:
oneWord = theWords.get(oneVal+10, "")
retVal = f"{hunWord} {oneWord}".strip()
else:
oneWord = theWords.get(oneVal, "")
if tenVal == 0:
retVal = f"{hunWord} {oneWord}".strip()
else:
tenWord = theWords.get(tenVal, "")
if oneVal == 0:
retVal = f"{hunWord} {tenWord}".strip()
else:
retVal = f"{hunWord} {tenWord}-{oneWord}".strip()
return retVal
def _numberToWord_NO(numVal, theCode):
"""Convert numbers to Norwegian words.
"""
oneWord = ""
tenWord = ""
hunWord = ""
if numVal == 0:
return "null"
oneVal = numVal % 10
tenVal = (numVal - oneVal) % 100
hunVal = (numVal - tenVal - oneVal) % 1000
theWords = {
0: "", 1: "én", 2: "to", 3: "tre", 4: "fire", 5: "fem", 6: "seks",
7: "sju", 8: "åtte", 9: "ni", 10: "ti", 11: "elleve", 12: "tolv",
13: "tretten", 14: "fjorten", 15: "femten", 16: "seksten", 17: "sytten",
18: "atten", 19: "nitten", 20: "tjue", 30: "tretti", 40: "førti",
50: "femti", 60: "seksti", 70: "sytti", 80: "åtti", 90: "nitti",
100: "ett hundre", 200: "to hundre", 300: "tre hundre", 400: "fire hundre",
500: "fem hundre", 600: "seks hundre", 700: "sju hundre",
800: "åtte hundre", 900: "ni hundre",
}
if theCode == "nn":
theWords[1] = "ein"
theWords[100] = "eitt hundre"
retVal = ""
hunWord = theWords.get(hunVal, "")
if tenVal == 10:
oneWord = theWords.get(oneVal+10, "")
sepVal = " og " if hunWord and oneWord else " "
retVal = f"{hunWord}{sepVal}{oneWord}"
else:
oneWord = theWords.get(oneVal, "")
if tenVal == 0:
sepVal = " og " if hunWord and oneWord else " "
retVal = f"{hunWord}{sepVal}{oneWord}".strip()
else:
tenWord = theWords.get(tenVal, "")
sepVal = " og " if hunWord and tenWord else " "
if oneVal == 0:
retVal = f"{hunWord}{sepVal}{tenWord}".strip()
else:
retVal = f"{hunWord}{sepVal}{tenWord}{oneWord}".strip()
return retVal.strip()
+4
View File
@@ -91,6 +91,7 @@ class GuiPreferences(PagedDialog):
logger.debug("Saving new preferences")
needsRestart = self.tabGeneral.saveValues()
prevSpell = self.mainConf.spellLanguage
self.tabProjects.saveValues()
self.tabDocs.saveValues()
@@ -98,6 +99,9 @@ class GuiPreferences(PagedDialog):
self.tabSyntax.saveValues()
self.tabAuto.saveValues()
if prevSpell != self.mainConf.spellLanguage:
self.theProject.loadProjectLocalisation(self.mainConf.spellLanguage)
if needsRestart:
self.theParent.makeAlert(
self.tr("Some changes will not be applied until novelWriter has been restarted."),
+7 -2
View File
@@ -112,11 +112,14 @@ def testCoreToken_Setters(dummyGUI):
# END Test testCoreToken_Setters
@pytest.mark.core
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI, tmpConf):
"""Test handling files and text in the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theProject.mainConf = tmpConf
theProject.projTree.setSeed(42)
theProject.loadProjectLocalisation("en")
theToken = Tokenizer(theProject, dummyGUI)
theToken.setKeepMarkdown(True)
@@ -408,10 +411,12 @@ def testCoreToken_Tokenize(dummyGUI):
# END Test testCoreToken_Tokenize
@pytest.mark.core
def testCoreToken_Headers(dummyGUI):
def testCoreToken_Headers(dummyGUI, tmpConf):
"""Test the header and page parser of the Tokenizer class.
"""
theProject = NWProject(dummyGUI)
theProject.mainConf = tmpConf
theProject.loadProjectLocalisation("en")
theToken = Tokenizer(theProject, dummyGUI)
# Nothing
+1 -78
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
from nw.core.tools import countWords, numberToRoman, numberToWord
from nw.core.tools import countWords, numberToRoman
@pytest.mark.core
def testCoreTools_CountWords():
@@ -81,80 +81,3 @@ def testCoreTools_RomanNumbers():
assert numberToRoman(999, True) == "cmxcix"
# END Test testCoreTools_RomanNumbers
@pytest.mark.core
def testCoreTools_NumberWords():
"""Test the conversion of integer to English words.
"""
# English
assert numberToWord(0, "en") == "Zero"
assert numberToWord(1, "en") == "One"
assert numberToWord(2, "en") == "Two"
assert numberToWord(3, "en") == "Three"
assert numberToWord(4, "en") == "Four"
assert numberToWord(5, "en") == "Five"
assert numberToWord(6, "en") == "Six"
assert numberToWord(7, "en") == "Seven"
assert numberToWord(8, "en") == "Eight"
assert numberToWord(9, "en") == "Nine"
assert numberToWord(10, "en") == "Ten"
assert numberToWord(11, "en") == "Eleven"
assert numberToWord(12, "en") == "Twelve"
assert numberToWord(13, "en") == "Thirteen"
assert numberToWord(14, "en") == "Fourteen"
assert numberToWord(15, "en") == "Fifteen"
assert numberToWord(16, "en") == "Sixteen"
assert numberToWord(17, "en") == "Seventeen"
assert numberToWord(18, "en") == "Eighteen"
assert numberToWord(19, "en") == "Nineteen"
assert numberToWord(20, "en") == "Twenty"
assert numberToWord(21, "en") == "Twenty-One"
assert numberToWord(29, "en") == "Twenty-Nine"
assert numberToWord(42, "en") == "Forty-Two"
assert numberToWord(114, "en") == "One Hundred Fourteen"
assert numberToWord(142, "en") == "One Hundred Forty-Two"
assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine"
# Norwegian
assert numberToWord(0, "nb") == "null"
assert numberToWord(1, "nb") == "én"
assert numberToWord(10, "nb") == "ti"
assert numberToWord(20, "nb") == "tjue"
assert numberToWord(21, "nb") == "tjueén"
assert numberToWord(29, "nb") == "tjueni"
assert numberToWord(42, "nb") == "førtito"
assert numberToWord(60, "nb") == "seksti"
assert numberToWord(100, "nb") == "ett hundre"
assert numberToWord(114, "nb") == "ett hundre og fjorten"
assert numberToWord(142, "nb") == "ett hundre og førtito"
assert numberToWord(999, "nb") == "ni hundre og nittini"
assert numberToWord(0, "nn") == "null"
assert numberToWord(1, "nn") == "ein"
assert numberToWord(10, "nn") == "ti"
assert numberToWord(20, "nn") == "tjue"
assert numberToWord(21, "nn") == "tjueein"
assert numberToWord(29, "nn") == "tjueni"
assert numberToWord(42, "nn") == "førtito"
assert numberToWord(60, "nn") == "seksti"
assert numberToWord(100, "nn") == "eitt hundre"
assert numberToWord(114, "nn") == "eitt hundre og fjorten"
assert numberToWord(142, "nn") == "eitt hundre og førtito"
assert numberToWord(999, "nn") == "ni hundre og nittini"
# Check complex language setting
assert numberToWord(1, "en_GB") == "One"
assert numberToWord(2, "en_GB") == "Two"
assert numberToWord(3, "en_GB") == "Three"
# Check a few with a nonsense language setting
assert numberToWord(1, "foo") == "1"
assert numberToWord(2, "foo") == "2"
assert numberToWord(3, "foo") == "3"
# Test out of range values
assert numberToWord(12345, "en") == "[>999]"
assert numberToWord(-2345, "en") == "[<0]"
assert numberToWord("234", "en") == "[NaN]"
# END Test testCoreTools_NumberWords