Grammalecte  Check-in [80ebc25208]

Overview
Comment:[core] ibdawg: suggestion mechanism update + keyboard chars proximity
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | core
Files: files | file ages | folders
SHA3-256: 80ebc25208ff6959e1d14608fd053a5ea72bd93723d9614c40835a550fd14130
User & Date: olr on 2017-06-26 06:50:06
Other Links: manifest | tags
Context
2017-06-26
06:54
[core] comment update check-in: 6288449780 user: olr tags: core, trunk
06:50
[core] ibdawg: suggestion mechanism update + keyboard chars proximity check-in: 80ebc25208 user: olr tags: core, trunk
2017-06-25
23:41
[core] ibdawg: suggestion mechanism update check-in: cee9fdd1aa user: olr tags: core, trunk
Changes

Modified gc_core/py/char_player.py from [faa9abdccc] to [b5981aec34].

   167    167       "ô": ("aut", "ot", "os"),
   168    168       "ö": ("aut", "ot", "os"),
   169    169   
   170    170       "u": ("ut", "us"),
   171    171   }
   172    172   
   173    173   dFinal2 = {
          174  +    "ai": ("aient", "ais", "et"),
   174    175       "an": ("ant", "ent"),
   175    176       "en": ("ent", "ant"),
   176    177       "ei": ("ait", "ais"),
   177    178       "on": ("ons", "ont"),
   178    179       "oi": ("ois", "oit", "oix"),
   179    180   }
   180    181   
   181    182   
   182    183   # Préfixes
   183    184   
   184         -aPfx = ("anti", "contre", "mé", "im", "in", "ir", "par", "pré", "re", "ré", "sans", "sous", "sur")
   185         -
   186         -
   187         -# Keyboards
   188         -
   189         -dBépo = {
   190         -    # on présume que le bépoète est moins susceptible de faire des erreurs de frappe que l’azertyste.
   191         -    # ligne 2
   192         -    "b": "éa",
   193         -    "é": "bpu",
   194         -    "p": "éoi",
   195         -    "o": "pèe",
   196         -    "è": "o",
   197         -    "v": "dt",
   198         -    "d": "vls",
   199         -    "l": "djr",
   200         -    "j": "lzn",
   201         -    "z": "jmw",
   202         -    # ligne 3
   203         -    "a": "ubà",
   204         -    "u": "aiéy",
   205         -    "i": "uepx",
   206         -    "e": "io",
   207         -    "c": "t",
   208         -    "t": "csvq",
   209         -    "s": "trdg",
   210         -    "r": "snlh",
   211         -    "n": "rmjf",
   212         -    "m": "nzç",
   213         -    # ligne 4
   214         -    "à": "yêa",
   215         -    "y": "àxu",
   216         -    "x": "ywi",
   217         -    "w": "z",
   218         -    "k": "c",
   219         -    "q": "gt",
   220         -    "g": "qhs",
   221         -    "h": "gfr",
   222         -    "f": "hçn",
   223         -    "ç": "fm",
   224         -}
   225         -
   226         -dAzerty = {
   227         -    # ligne 1
   228         -    "é": "az",
   229         -    "è": "yu",
   230         -    "ç": "àio",
   231         -    "à": "op",
   232         -    # ligne 2
   233         -    "a": "zéqs",
   234         -    "z": "aesqd",
   235         -    "e": "zrdsf",
   236         -    "r": "etfdg",
   237         -    "t": "rygfh",
   238         -    "y": "tuhgj",
   239         -    "u": "yijhk",
   240         -    "i": "uokjl",
   241         -    "o": "iplkm",
   242         -    "p": "oml",
   243         -    # ligne 3
   244         -    "q": "sawz",
   245         -    "s": "qdzwxe",
   246         -    "d": "sfexcr",
   247         -    "f": "dgrcvt",
   248         -    "g": "fhtvby",
   249         -    "h": "gjybnu",
   250         -    "j": "hkuni",
   251         -    "k": "jlio",
   252         -    "l": "kmop",
   253         -    "m": "lùp",
   254         -    "ù": "m",
   255         -    # ligne 4
   256         -    "w": "xqs",
   257         -    "x": "wcsd",
   258         -    "c": "xvdf",
   259         -    "v": "cbfg",
   260         -    "b": "vngh",
   261         -    "n": "bhj",
   262         -}
          185  +aPfx1 = frozenset([
          186  +    "anti", "archi", "contre", "hyper", "mé", "méta", "im", "in", "ir", "par", "proto",
          187  +    "pseudo", "pré", "re", "ré", "sans", "sous", "supra", "sur", "ultra"
          188  +])
          189  +aPfx2 = frozenset([
          190  +    "belgo", "franco", "génito", "gynéco", "médico", "russo"
          191  +])

Modified gc_core/py/ibdawg.py from [18fa7e7c19] to [1a4cdd2a3d].

   130    130                               "_addrBitMask": self._addrBitMask,
   131    131                               "nBytesOffset": self.nBytesOffset
   132    132                           }, ensure_ascii=False))
   133    133               if bInJSModule:
   134    134                   hDst.write(";\n\nexports.dictionary = dictionary;\n")
   135    135   
   136    136       def isValidToken (self, sToken):
   137         -        "checks if sToken is valid (if there is hyphens in sToken, sToken is split, each part is checked)"
          137  +        "checks if <sToken> is valid (if there is hyphens in <sToken>, <sToken> is split, each part is checked)"
   138    138           if self.isValid(sToken):
   139    139               return True
   140    140           if "-" in sToken:
   141    141               if sToken.count("-") > 4:
   142    142                   return True
   143    143               return all(self.isValid(sWord)  for sWord in sToken.split("-"))
   144    144           return False
   145    145   
   146    146       def isValid (self, sWord):
   147         -        "checks if sWord is valid (different casing tested if the first letter is a capital)"
          147  +        "checks if <sWord> is valid (different casing tested if the first letter is a capital)"
   148    148           if not sWord:
   149    149               return None
   150    150           if "’" in sWord: # ugly hack
   151    151               sWord = sWord.replace("’", "'")
   152    152           if self.lookup(sWord):
   153    153               return True
   154    154           if sWord[0:1].isupper():
................................................................................
   161    161                       return bool(self.lookup(sWord.lower()) or self.lookup(sWord.capitalize()))
   162    162                   return bool(self.lookup(sWord[:1].lower() + sWord[1:]))
   163    163               else:
   164    164                   return bool(self.lookup(sWord.lower()))
   165    165           return False
   166    166   
   167    167       def lookup (self, sWord):
   168         -        "returns True if sWord in dictionary (strict verification)"
          168  +        "returns True if <sWord> in dictionary (strict verification)"
   169    169           iAddr = 0
   170    170           for c in sWord:
   171    171               if c not in self.dChar:
   172    172                   return False
   173    173               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   174    174               if iAddr == None:
   175    175                   return False
   176    176           return int.from_bytes(self.byDic[iAddr:iAddr+self.nBytesArc], byteorder='big') & self._finalNodeMask
   177    177   
   178    178       def suggest (self, sWord):
   179    179           "returns a set of similar words"
   180    180           # first, we check for similar words
   181         -        return set(self._suggestWithCrushedUselessChars(cp.clearWord(sWord)))
          181  +        #return set(self._suggestWithCrushedUselessChars(cp.clearWord(sWord)))
   182    182           lSugg = self._suggest(sWord)
   183    183           if not lSugg:
   184    184               lSugg.extend(self._suggest(sWord[1:]))
   185    185               lSugg.extend(self._suggest(sWord[:-1]))
   186    186               lSugg.extend(self._suggest(sWord[1:-1]))
   187    187               if not lSugg:
   188    188                   lSugg.extend(self._suggestWithCrushedUselessChars(cp.clearWord(sWord)))
   189    189           return set(lSugg)
   190    190   
   191         -    def _suggest (self, sWord, cPrevious='', nDeep=0, iAddr=0, sNewWord="", bAvoidLoop=False):
          191  +    def _suggest (self, sWord, nDeep=0, iAddr=0, sNewWord="", bAvoidLoop=False):
   192    192           # RECURSIVE FUNCTION
   193    193           if not sWord:
   194    194               if int.from_bytes(self.byDic[iAddr:iAddr+self.nBytesArc], byteorder='big') & self._finalNodeMask:
   195    195                   show(nDeep, "!!! " + sNewWord + " !!!")
   196    196                   return [sNewWord]
   197    197               return []
   198    198           #show(nDeep, "<" + sWord + ">  ===>  " + sNewWord)
   199    199           lSugg = []
   200    200           cCurrent = sWord[0:1]
   201    201           for cChar, jAddr in self._getSimilarArcs(cCurrent, iAddr):
   202    202               #show(nDeep, cChar)
   203         -            lSugg.extend(self._suggest(sWord[1:], cCurrent, nDeep+1, jAddr, sNewWord+cChar))
          203  +            lSugg.extend(self._suggest(sWord[1:], nDeep+1, jAddr, sNewWord+cChar))
   204    204           if not bAvoidLoop: # avoid infinite loop
   205    205               #show(nDeep, ":no loop:")
   206         -            if cPrevious == cCurrent:
          206  +            if cCurrent == sWord[1:2]:
   207    207                   # same char, we remove 1 char without adding 1 to <sNewWord>
   208         -                lSugg.extend(self._suggest(sWord[1:], cCurrent, nDeep+1, iAddr, sNewWord))
          208  +                lSugg.extend(self._suggest(sWord[1:], nDeep+1, iAddr, sNewWord))
   209    209               for sRepl in cp.d1toX.get(cCurrent, ()):
   210    210                   #show(nDeep, sRepl)
   211         -                lSugg.extend(self._suggest(sRepl + sWord[1:], cCurrent, nDeep+1, iAddr, sNewWord, True))
   212         -            if len(sWord) == 1:
          211  +                lSugg.extend(self._suggest(sRepl + sWord[1:], nDeep+1, iAddr, sNewWord, True))
          212  +            if len(sWord) == 2:
          213  +                for sRepl in cp.dFinal2.get(sWord, ()):
          214  +                    #show(nDeep, sRepl)
          215  +                    lSugg.extend(self._suggest(sRepl, nDeep+1, iAddr, sNewWord, True))
          216  +            elif len(sWord) == 1:
   213    217                   #show(nDeep, ":end of word:")
   214    218                   # end of word
   215    219                   for sRepl in cp.dFinal1.get(sWord, ()):
   216    220                       #show(nDeep, sRepl)
   217         -                    lSugg.extend(self._suggest(sRepl, cCurrent, nDeep+1, iAddr, sNewWord, True))
          221  +                    lSugg.extend(self._suggest(sRepl, nDeep+1, iAddr, sNewWord, True))
   218    222           return lSugg
   219    223   
   220    224       def _getSimilarArcs (self, cChar, iAddr):
   221    225           "generator: yield similar char of <cChar> and address of the following node"
   222    226           for c in cp.d1to1.get(cChar, [cChar]):
   223    227               if c in self.dChar:
   224    228                   jAddr = self._lookupArcNode(self.dChar[c], iAddr)
   225    229                   if jAddr:
   226    230                       yield (c, jAddr)
   227    231   
   228         -    def _suggestWithCrushedUselessChars (self, sWord, cPrevious='', nDeep=0, iAddr=0, sNewWord="", bAvoidLoop=False):
          232  +    def _suggestWithCrushedUselessChars (self, sWord, nDeep=0, iAddr=0, sNewWord="", bAvoidLoop=False):
   229    233           if not sWord:
   230    234               if int.from_bytes(self.byDic[iAddr:iAddr+self.nBytesArc], byteorder='big') & self._finalNodeMask:
   231    235                   show(nDeep, "!!! " + sNewWord + " !!!")
   232    236                   return [sNewWord]
   233    237               return []
   234    238           lSugg = []
   235    239           cCurrent = sWord[0:1]
   236    240           for cChar, jAddr in self._getSimilarArcsAndCrushedChars(cCurrent, iAddr):
   237    241               show(nDeep, cChar)
   238         -            lSugg.extend(self._suggestWithCrushedUselessChars(sWord[1:], cCurrent, nDeep+1, jAddr, sNewWord+cChar))
          242  +            lSugg.extend(self._suggestWithCrushedUselessChars(sWord[1:], nDeep+1, jAddr, sNewWord+cChar))
   239    243           return lSugg
   240    244   
   241    245       def _getSimilarArcsAndCrushedChars (self, cChar, iAddr):
   242    246           "generator: yield similar char of <cChar> and address of the following node"
   243    247           for nVal, jAddr in self._getArcs(iAddr):
   244    248               if self.dVal.get(nVal, "") in cp.aUselessChar:
   245    249                   yield (self.dVal[nVal], jAddr)
................................................................................
   290    294                           l.append(sStem + " " + self.lArcVal[nRawArc2 & self._arcMask])
   291    295                           iAddr2 = iEndArcAddr2+self.nBytesNodeAddress
   292    296                   iAddr = iEndArcAddr+self.nBytesNodeAddress
   293    297               return l
   294    298           return []
   295    299   
   296    300       def _stem1 (self, sWord):
   297         -        "returns stems list of sWord"
          301  +        "returns stems list of <sWord>"
   298    302           iAddr = 0
   299    303           for c in sWord:
   300    304               if c not in self.dChar:
   301    305                   return []
   302    306               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   303    307               if iAddr == None:
   304    308                   return []
................................................................................
   313    317                       # This value is not a char, this is a stemming code 
   314    318                       l.append(self.funcStemming(sWord, self.lArcVal[nArc]))
   315    319                   iAddr = iEndArcAddr+self.nBytesNodeAddress
   316    320               return l
   317    321           return []
   318    322   
   319    323       def _lookupArcNode1 (self, nVal, iAddr):
   320         -        "looks if nVal is an arc at the node at iAddr, if yes, returns address of next node else None"
          324  +        "looks if <nVal> is an arc at the node at <iAddr>, if yes, returns address of next node else None"
   321    325           while True:
   322    326               iEndArcAddr = iAddr+self.nBytesArc
   323    327               nRawArc = int.from_bytes(self.byDic[iAddr:iEndArcAddr], byteorder='big')
   324    328               if nVal == (nRawArc & self._arcMask):
   325    329                   # the value we are looking for 
   326    330                   # we return the address of the next node
   327    331                   return int.from_bytes(self.byDic[iEndArcAddr:iEndArcAddr+self.nBytesNodeAddress], byteorder='big')
................................................................................
   357    361                   iAddr = iEndArcAddr+self.nBytesNodeAddress
   358    362                   if (nRawArc & self._lastArcMask) and iAddr < len(self.byDic):
   359    363                       hDst.write("\ni{:_>10} -- #{:_>10}\n".format("?", iAddr))
   360    364               hDst.close()
   361    365   
   362    366       # VERSION 2
   363    367       def _morph2 (self, sWord):
   364         -        "returns morphologies of sWord"
          368  +        "returns morphologies of <sWord>"
   365    369           iAddr = 0
   366    370           for c in sWord:
   367    371               if c not in self.dChar:
   368    372                   return []
   369    373               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   370    374               if iAddr == None:
   371    375                   return []
................................................................................
   395    399                           l.append(sStem + " " + self.lArcVal[nRawArc2 & self._arcMask])
   396    400                           iAddr2 = iEndArcAddr2+self.nBytesNodeAddress  if not (nRawArc2 & self._addrBitMask) else iEndArcAddr2
   397    401                   iAddr = iEndArcAddr+self.nBytesNodeAddress  if not (nRawArc & self._addrBitMask)  else iEndArcAddr
   398    402               return l
   399    403           return []
   400    404   
   401    405       def _stem2 (self, sWord):
   402         -        "returns stems list of sWord"
          406  +        "returns stems list of <sWord>"
   403    407           iAddr = 0
   404    408           for c in sWord:
   405    409               if c not in self.dChar:
   406    410                   return []
   407    411               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   408    412               if iAddr == None:
   409    413                   return []
................................................................................
   427    431                               nRawArc = int.from_bytes(self.byDic[iAddr2:iAddr2+self.nBytesArc], byteorder='big')
   428    432                               iAddr2 += self.nBytesArc + self.nBytesNodeAddress
   429    433                   iAddr = iEndArcAddr+self.nBytesNodeAddress  if not (nRawArc & self._addrBitMask)  else iEndArcAddr
   430    434               return l
   431    435           return []
   432    436   
   433    437       def _lookupArcNode2 (self, nVal, iAddr):
   434         -        "looks if nVal is an arc at the node at iAddr, if yes, returns address of next node else None"
          438  +        "looks if <nVal> is an arc at the node at <iAddr>, if yes, returns address of next node else None"
   435    439           while True:
   436    440               iEndArcAddr = iAddr+self.nBytesArc
   437    441               nRawArc = int.from_bytes(self.byDic[iAddr:iEndArcAddr], byteorder='big')
   438    442               if nVal == (nRawArc & self._arcMask):
   439    443                   # the value we are looking for 
   440    444                   if not (nRawArc & self._addrBitMask):
   441    445                       # we return the address of the next node
................................................................................
   472    476                       iAddr = iEndArcAddr
   473    477                   if (nRawArc & self._lastArcMask):
   474    478                       hDst.write("\ni{:_>10} -- #{:_>10}\n".format("?", iAddr))
   475    479               hDst.close()
   476    480   
   477    481       # VERSION 3
   478    482       def _morph3 (self, sWord):
   479         -        "returns morphologies of sWord"
          483  +        "returns morphologies of <sWord>"
   480    484           iAddr = 0
   481    485           for c in sWord:
   482    486               if c not in self.dChar:
   483    487                   return []
   484    488               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   485    489               if iAddr == None:
   486    490                   return []
................................................................................
   507    511                           l.append(sStem + " " + self.lArcVal[nRawArc2 & self._arcMask])
   508    512                           iAddr2 = iEndArcAddr2+self.nBytesNodeAddress  if not (nRawArc2 & self._addrBitMask) else iEndArcAddr2+self.nBytesOffset
   509    513                   iAddr = iEndArcAddr+self.nBytesNodeAddress  if not (nRawArc & self._addrBitMask)  else iEndArcAddr+self.nBytesOffset
   510    514               return l
   511    515           return []
   512    516   
   513    517       def _stem3 (self, sWord):
   514         -        "returns stems list of sWord"
          518  +        "returns stems list of <sWord>"
   515    519           iAddr = 0
   516    520           for c in sWord:
   517    521               if c not in self.dChar:
   518    522                   return []
   519    523               iAddr = self._lookupArcNode(self.dChar[c], iAddr)
   520    524               if iAddr == None:
   521    525                   return []
................................................................................
   531    535                       # This value is not a char, this is a stemming code 
   532    536                       l.append(self.funcStemming(sWord, self.lArcVal[nArc]))
   533    537                   iAddr = iEndArcAddr+self.nBytesNodeAddress  if not (nRawArc & self._addrBitMask)  else iEndArcAddr+self.nBytesOffset
   534    538               return l
   535    539           return []
   536    540   
   537    541       def _lookupArcNode3 (self, nVal, iAddr):
   538         -        "looks if nVal is an arc at the node at iAddr, if yes, returns address of next node else None"
          542  +        "looks if <nVal> is an arc at the node at <iAddr>, if yes, returns address of next node else None"
   539    543           iAddrNode = iAddr
   540    544           while True:
   541    545               iEndArcAddr = iAddr+self.nBytesArc
   542    546               nRawArc = int.from_bytes(self.byDic[iAddr:iEndArcAddr], byteorder='big')
   543    547               if nVal == (nRawArc & self._arcMask):
   544    548                   # the value we are looking for 
   545    549                   if not (nRawArc & self._addrBitMask):

Added gc_core/py/keyboard_chars_proximity.py version [d1b0d3e0b7].

            1  +# Keyboard chars proximity
            2  +
            3  +
            4  +def getKeyboardMap (sKeyboard):
            5  +    return _dKeyboardMap.get(sKeyboard.lower(), {})
            6  +
            7  +
            8  +def getKeyboardList ():
            9  +    return _dKeyboardMap.keys()
           10  +
           11  +
           12  +# bépo, colemak and dvorak users are assumed to do less typing errors.
           13  +_dKeyboardMap = {
           14  +    "azerty": {
           15  +        # fr
           16  +        # line 1
           17  +        "é": "az",
           18  +        "è": "yu",
           19  +        "ç": "àio",
           20  +        "à": "op",
           21  +        # line 2
           22  +        "a": "zéq",
           23  +        "z": "aesq",
           24  +        "e": "zrds",
           25  +        "r": "etfd",
           26  +        "t": "rygf",
           27  +        "y": "tuhg",
           28  +        "u": "yijh",
           29  +        "i": "uokj",
           30  +        "o": "iplk",
           31  +        "p": "oml",
           32  +        # line 3
           33  +        "q": "sawz",
           34  +        "s": "qdzwxe",
           35  +        "d": "sfexcr",
           36  +        "f": "dgrcvt",
           37  +        "g": "fhtvby",
           38  +        "h": "gjybnu",
           39  +        "j": "hkuni",
           40  +        "k": "jlio",
           41  +        "l": "kmop",
           42  +        "m": "lùp",
           43  +        "ù": "m",
           44  +        # line 4
           45  +        "w": "xqs",
           46  +        "x": "wcsd",
           47  +        "c": "xvdf",
           48  +        "v": "cbfg",
           49  +        "b": "vngh",
           50  +        "n": "bhj",
           51  +    },
           52  +    "bépo": {
           53  +        # fr
           54  +        # line 2
           55  +        "b": "éa",
           56  +        "é": "bpu",
           57  +        "p": "éoi",
           58  +        "o": "pèe",
           59  +        "è": "o",
           60  +        "v": "dt",
           61  +        "d": "vls",
           62  +        "l": "djr",
           63  +        "j": "lzn",
           64  +        "z": "jmw",
           65  +        # line 3
           66  +        "a": "ubà",
           67  +        "u": "aiéy",
           68  +        "i": "uepx",
           69  +        "e": "io",
           70  +        "c": "t",
           71  +        "t": "csvq",
           72  +        "s": "trdg",
           73  +        "r": "snlh",
           74  +        "n": "rmjf",
           75  +        "m": "nzç",
           76  +        # line 4
           77  +        "à": "yêa",
           78  +        "y": "àxu",
           79  +        "x": "ywi",
           80  +        "w": "z",
           81  +        "k": "c",
           82  +        "q": "gt",
           83  +        "g": "qhs",
           84  +        "h": "gfr",
           85  +        "f": "hçn",
           86  +        "ç": "fm",
           87  +    },
           88  +    "colemak": {
           89  +        # en, us, intl
           90  +        # line 2
           91  +        "q": "wa",
           92  +        "w": "qfr",
           93  +        "f": "wps",
           94  +        "p": "fgt",
           95  +        "g": "pjd",
           96  +        "j": "glh",
           97  +        "l": "jun",
           98  +        "u": "lye",
           99  +        "y": "ui",
          100  +        # line 3
          101  +        "a": "rqz",
          102  +        "r": "aswx",
          103  +        "s": "rtfc",
          104  +        "t": "sdpv",
          105  +        "d": "thgb",
          106  +        "h": "dnjk",
          107  +        "n": "helm",
          108  +        "e": "niu",
          109  +        "i": "eoy",
          110  +        "o": "i",
          111  +        # line 4
          112  +        "z": "xa",
          113  +        "x": "zcr",
          114  +        "c": "xvs",
          115  +        "v": "cbt",
          116  +        "b": "vkd",
          117  +        "k": "bmh",
          118  +        "m": "kn",
          119  +    },
          120  +    "dvorak": {
          121  +        # en, us, intl
          122  +        # line 2
          123  +        "p": "yu",
          124  +        "y": "pfi",
          125  +        "f": "ygd",
          126  +        "g": "fch",
          127  +        "c": "grt",
          128  +        "r": "cln",
          129  +        "l": "rs",
          130  +        # line 3
          131  +        "a": "o",
          132  +        "o": "aeq",
          133  +        "e": "ouj",
          134  +        "u": "eipk",
          135  +        "i": "udyx",
          136  +        "d": "ihfb",
          137  +        "h": "dtgm",
          138  +        "t": "hncw",
          139  +        "n": "tsrv",
          140  +        "s": "nlz",
          141  +        # line 4
          142  +        "q": "jo",
          143  +        "j": "qke",
          144  +        "k": "jxu",
          145  +        "x": "kbi",
          146  +        "b": "xmd",
          147  +        "m": "bwh",
          148  +        "w": "mvt",
          149  +        "v": "wzn",
          150  +        "z": "vs",
          151  +    },
          152  +    "qwerty": {
          153  +        # en, us, intl
          154  +        # line 2
          155  +        "q": "wa",
          156  +        "w": "qeas",
          157  +        "e": "wrds",
          158  +        "r": "etfd",
          159  +        "t": "rygf",
          160  +        "y": "tuhg",
          161  +        "u": "yijh",
          162  +        "i": "uokj",
          163  +        "o": "iplk",
          164  +        "p": "ol",
          165  +        # line 3
          166  +        "a": "sqzw",
          167  +        "s": "adwzxe",
          168  +        "d": "sfexcr",
          169  +        "f": "dgrcvt",
          170  +        "g": "fhtvby",
          171  +        "h": "gjybnu",
          172  +        "j": "hkunmi",
          173  +        "k": "jlimo",
          174  +        "l": "kop",
          175  +        # line 4
          176  +        "z": "xas",
          177  +        "x": "zcsd",
          178  +        "c": "xvdf",
          179  +        "v": "cbfg",
          180  +        "b": "vngh",
          181  +        "n": "bmhj",
          182  +        "m": "njk",
          183  +    },
          184  +    "qwertz": {
          185  +        # ge, au
          186  +        # line 2
          187  +        "q": "wa",
          188  +        "w": "qeas",
          189  +        "e": "wrds",
          190  +        "r": "etfd",
          191  +        "t": "rzgf",
          192  +        "z": "tuhg",
          193  +        "u": "zijh",
          194  +        "i": "uokj",
          195  +        "o": "iplk",
          196  +        "p": "oüöl",
          197  +        "ü": "päö",
          198  +        # line 3
          199  +        "a": "sqyw",
          200  +        "s": "adwyxe",
          201  +        "d": "sfexcr",
          202  +        "f": "dgrcvt",
          203  +        "g": "fhtvbz",
          204  +        "h": "gjzbnu",
          205  +        "j": "hkunmi",
          206  +        "k": "jlimo",
          207  +        "l": "köop",
          208  +        "ö": "läpü",
          209  +        "ä": "öü",
          210  +        # line 4
          211  +        "y": "xas",
          212  +        "x": "ycsd",
          213  +        "c": "xvdf",
          214  +        "v": "cbfg",
          215  +        "b": "vngh",
          216  +        "n": "bmhj",
          217  +        "m": "njk",
          218  +    }
          219  +}