Thành viên:Plantaest/Blog/Xây dựng bộ gõ tiếng Việt trên nền jQuery.IME
Tài liệu này lưu lại quá trình nghiên cứu, phân tích và triển khai xây dựng bộ gõ tiếng Việt trên nền jQuery.IME.
Lịch sử
sửaNgày 18 tháng 7 năm 2021, một sự cố của AVIM đã được cộng đồng Wikipedia tiếng Việt nhận thấy, và Bluetpp đã báo cáo tại Phabricator.
Ngày 17 tháng 8 năm 2021, sau một thời gian nghiên cứu, tôi đã viết được thuật toán của bộ gõ tiếng Việt trên nền jQuery.IME thông qua một bài viết về âm tiết tiếng Việt của tác giả Hieu-Thi Luong: All syllables in Vietnamese language, với khoảng 400 dòng mã. Cơ chế của thuật toán này là sử dụng một hệ thống regex phức tạp để điều phối các hoạt động tạo hình thể cho các âm tiết tiếng Việt, từ đó tạo ra cơ chế gõ tiếng Việt.
Tuy nhiên, do quá lâu không quan tâm nữa nên dự án này đi vào quên lãng, và hiện tại tôi không còn hiểu thuật toán này. Nguyên nhân năm 2021 không tiếp tục triển khai dự án này là do muốn giải quyết một vấn đề về hiệu suất trước khi phát hành, nhưng sau lại quên và không còn để ý nữa. Tuy vậy, tôi đã lưu lại mã nguồn và một số ghi chú trên Notion mô tả cách nó hoạt động. Hy vọng sẽ hoàn thành phát triển dự án này trong thời gian sớm nhất.
Năm 2024, dự án này được đặt tên là VIWP.IME, dự kiến tái triển khai vào đầu năm 2025, nhằm mục tiêu loại bỏ hoàn toàn AVIM và thay bằng ULS.
Mã nguồn năm 2021
sửa
Mã nguồn tệp vi-vni-lint.js
, lint có nghĩa là bản đã được ESLint định dạng lại theo chuẩn của WMF.
/* eslint-disable no-console */
( function ( $ ) {
var vowels, vowelsMark,
oldStyle, keys, placeholders, diaIdxs, moveToneExps, funcs,
patterns, puncts, patterns_shift, vi;
function buildIdxs( keyList, idx, div ) {
var idxs = {};
keyList.split( '' ).forEach( function ( key ) {
idxs[ key + ( div || '' ) ] = idx;
} );
return idxs;
}
vowels = 'aáàảãạăắằẳẵặâấầẩẫậeéèẻẽẹêếềểễệiíìỉĩịoóòỏõọôốồổỗộơớờởỡợuúùủũụưứừửữựyýỳỷỹỵ';
vowelsMark = 'aâ_ăeê__oôơ_u_ư_';
oldStyle = true;
keys = {
SAC: '1',
HUYEN: '2',
HOI: '3',
NGA: '4',
NANG: '5',
MU_A: '6',
MU_E: '6',
MU_O: '6',
MOC_O: '7',
MOC_U: '7',
TRANG: '8',
D_NGANG: '9',
XOA_DAU: '0'
};
placeholders = {
'@A': keys.SAC,
'@B': keys.HUYEN,
'@C': keys.HOI,
'@D': keys.NGA,
'@E': keys.NANG,
'@F': keys.MU_A,
'@G': keys.MU_E,
'@H': keys.MU_O,
'@I': keys.MOC_O,
'@J': keys.MOC_U,
'@K': keys.TRANG,
'@L': keys.D_NGANG,
'@M': keys.XOA_DAU,
'%B': '\\b'
};
// Ký tự phím => Index dấu thanh/phụ
diaIdxs = $.extend( {},
buildIdxs( keys.SAC, 1 ),
buildIdxs( keys.HUYEN, 2 ),
buildIdxs( keys.HOI, 3 ),
buildIdxs( keys.NGA, 4 ),
buildIdxs( keys.NANG, 5 ),
buildIdxs( keys.MU_E, 1 ),
buildIdxs( keys.MU_A, 1 ),
buildIdxs( keys.MU_O, 1 ),
buildIdxs( keys.MOC_O, 2, 1 ),
buildIdxs( keys.MOC_U, 2, 1 ),
buildIdxs( keys.TRANG, 3, 2 )
);
moveToneExps = {
uo2: 'ươ', // Thêm dấu râu bổ sung cho ký tự u
ưo0: 'ươ', // Thêm dấu râu bổ sung cho ký tự o
Ưo0: 'Ươ',
UO2: 'ƯƠ',
ƯO0: 'ƯƠ'
};
function vowelIdx( vowel ) {
return vowels.indexOf( vowel.toLowerCase() );
}
function toneIdx( vowel ) {
return vowels.indexOf( vowel.toLowerCase() ) % 6;
}
function isOldStyle( vowelGroup, hasLastConsonant ) {
return oldStyle && vowelGroup.length === 2 && /[aey]$/gi.test( vowelGroup ) && !hasLastConsonant;
}
function checkUpperCase( strCondition, strReturn ) {
return strCondition === strCondition.toUpperCase() ? strReturn.toUpperCase() : strReturn;
}
function extractVowel( vowelGroup, hasLastConsonant ) {
if ( isOldStyle( vowelGroup, hasLastConsonant ) ) {
return vowelGroup[ 0 ];
}
return vowelGroup.slice( -1 );
}
function delTone( vowelGroup, hasLastConsonant ) {
var vowel = extractVowel( vowelGroup, hasLastConsonant ),
vowelBase = vowels[ vowelIdx( vowel ) - toneIdx( vowel ) ];
if ( isOldStyle( vowelGroup, hasLastConsonant ) ) {
return vowelGroup.replace( /^./g, checkUpperCase( vowelGroup, vowelBase ) );
}
return vowelGroup.replace( /.$/g, checkUpperCase( vowelGroup, vowelBase ) );
}
function addTone( vowelGroup, newToneIdx, hasLastConsonant ) {
var vowelBase = delTone( extractVowel( vowelGroup, hasLastConsonant ) ),
vowelBaseWithTone = vowels[ vowelIdx( vowelBase ) + newToneIdx ];
if ( isOldStyle( vowelGroup, hasLastConsonant ) ) {
return vowelGroup.replace( /^./g, checkUpperCase( vowelGroup, vowelBaseWithTone ) );
}
return vowelGroup.replace( /.$/g, checkUpperCase( vowelGroup, vowelBaseWithTone ) );
}
function vowelMarkIdx( vowelMark ) {
return vowelsMark.indexOf( vowelMark.toLowerCase() );
}
function markIdx( vowelMark ) {
return vowelsMark.indexOf( vowelMark.toLowerCase() ) % 4;
}
function delMark( vowel ) {
var oldToneIdx = toneIdx( vowel ),
vowelMark = delTone( vowel ),
vowelRoot = vowelsMark[ vowelMarkIdx( vowelMark ) - markIdx( vowelMark ) ];
if ( oldToneIdx !== 0 ) {
return addTone( checkUpperCase( vowel, vowelRoot ), oldToneIdx );
}
return checkUpperCase( vowel, vowelRoot );
}
function addMark( vowel, newMarkIdx ) {
var oldToneIdx = toneIdx( vowel ),
vowelRoot = delMark( delTone( vowel ) ),
vowelRootWithMark = vowelsMark[ vowelMarkIdx( vowelRoot ) + newMarkIdx ];
if ( oldToneIdx !== 0 ) {
return addTone( checkUpperCase( vowel, vowelRootWithMark ), oldToneIdx );
}
return checkUpperCase( vowel, vowelRootWithMark );
}
function tone( $0, $1, $2, $3, $4, $5 ) {
var toneIdxFromVowel = toneIdx( extractVowel( $2, !!$3 ) ),
toneIdxFromKey = diaIdxs[ $5 ],
main, last;
console.log( 'tone(): ' + $0 + ' - ' + $1 + ' - ' + $2 + ' - ' + $3 + ' - ' + $4 + ' - ' + $5 );
if ( toneIdxFromVowel === toneIdxFromKey ) {
return $1 + delTone( $2, !!$3 ) + ( $3 || '' ) + $5;
}
if ( $4 && [ 2, 3, 4 ].indexOf( toneIdxFromKey ) >= 0 ) {
return $0;
}
main = addTone( $2, toneIdxFromKey, !!$3 );
last = $3 || '';
return $1 + main + last;
}
function mark( $0, $1, $2, $3, $4, div ) {
var markIdxFromVowel = markIdx( delTone( $2 ) ),
markIdxFromKey = diaIdxs[ $4 + ( div || '' ) ],
main, last;
console.log( 'mark(): ' + $0 + ' - ' + $1 + ' - ' + $2 + ' - ' + $3 + ' - ' + $4 + ' - ' + div );
if ( $4 && markIdxFromVowel === markIdxFromKey ) {
return delMark( $1 ) + delMark( $2 ) + ( $3 || '' ) + $4;
}
main = addMark( $2, ( $4 ? markIdxFromKey : 2 ) );
last = $3 || '';
return $1 + main + last;
}
function stroke( $0, $1, $2, $6 ) {
if ( /[đ]/gi.test( $1 ) ) {
return ( $1 === 'đ' ? 'd' : 'D' ) + ( $2 || '' ) + $6;
}
return ( $1 === 'd' ? 'đ' : 'Đ' ) + ( $2 || '' );
}
function toneless( $0, $1, $2, $3, $4 ) {
var onset = $1 || '',
main = ( $2 || '' ) + delTone( $3 ),
last = $4 || '';
console.log( 'toneless(): ' + $0 + ' - ' + $1 + ' - ' + $2 + ' - ' + $3 + ' - ' + $4 );
return onset + main + last;
}
function moveTone( $0, $1, $2, $3, $4, div ) {
var startsWithU = /u/gi.test( $2[ 0 ] ),
tonedChar = startsWithU ? $2[ 1 ] : $2[ 0 ],
toneIdxFromTonedChar = toneIdx( tonedChar ),
markIdxFromKey = diaIdxs[ $4 + ( div || '' ) ],
vowelGroupDelTone = startsWithU ? $2[ 0 ] + delTone( $2[ 1 ] ) + $2[ 2 ] : delTone( $2[ 0 ] ) + $2.slice( 1 ),
expCase = moveToneExps[ vowelGroupDelTone + ( markIdxFromKey || 0 ) ],
main = addTone( ( expCase || ( $4 ? vowelGroupDelTone.slice( 0, -1 ) + addMark( vowelGroupDelTone.slice( -1 ), markIdxFromKey ) : vowelGroupDelTone ) ), toneIdxFromTonedChar, true ),
last = $3 || '';
console.log( 'moveTone(): ' + $0 + ' - ' + $1 + ' - ' + $2 + ' - ' + $3 + ' - ' + $4 + ' - ' + div );
return $1 + main + last;
}
funcs = {
tone: function ( $0, $1, $2, $3, $4, $5 ) { return tone( $0, $1, $2, $3, $4, $5 ); },
moveTone: function ( $0, $1, $2, $3, $4 ) { return moveTone( $0, $1, $2, $3, $4 ); },
moveTone$1: function ( $0, $1, $2, $3, $4 ) { return moveTone( $0, $1, $2, $3, $4, 1 ); },
moveTone$2: function ( $0, $1, $2, $3, $4 ) { return moveTone( $0, $1, $2, $3, $4, 2 ); },
moveTone$3: function ( $0, $1, $2, $3 ) { return moveTone( $0, $1, $2, $3 ); },
moveTone$4: function ( $0, $1, $2 ) { return moveTone( $0, $1, $2 ); },
mark: function ( $0, $1, $2, $3, $4 ) { return mark( $0, $1, $2, $3, $4 ); },
mark$1: function ( $0, $1, $2, $3, $4 ) { return mark( $0, $1, $2, $3, $4, 1 ); },
mark$2: function ( $0, $1, $2, $3, $4 ) { return mark( $0, $1, $2, $3, $4, 2 ); },
mark$3: function ( $0, $1, $2, $3 ) { return mark( $0, $1, $2, $3 ); },
mark$del: function ( $0, $1, $2, $3, $4 ) { return mark( $0, delMark( $1 ), $2, $3, $4 ); },
mark$add: function ( $0, $1, $2, $3, $4 ) { return mark( $0, addMark( $1, 2 ), $2, $3, $4, 1 ); },
stroke: function ( $0, $1, $2, $3, $4, $5, $6 ) { return stroke( $0, $1, $2, $6 ); },
toneless: function ( $0, $1, $2, $3, $4 ) { return toneless( $0, $1, $2, $3, $4 ); }
};
patterns = [
// A. 25 PATTERN GIEO DẤU THANH CHO 25 ÂM CHÍNH
[ '(^|[^iíìỉĩịuúùủũụưứừửữựyýỳỷỹỵoóòỏõọ]|qu|gi|Qu|Gi)([aáàảãạ])([iouymn]|ng|nh|(ch|[ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^o])([ăắằẳẵặ])([mn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^u]|qu|Qu)([âấầẩẫậ])([uymn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^oóòỏõọ])([eéèẻẽẹ])([omn]|ng|([ctp]|ch)|u|nh)?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^iuy]|qu|gi|Qu|Gi)([êếềểễệ])([umn]|nh|(ch|[ctp])|ng)?([@A@B@C@D@E])', funcs.tone ],
// Đặt /u/ trước /i/ để có được từ "giặt giũ"
[ '(^|[^aáàảãạâấầẩẫậêếềểễệeéèẻẽẹiíìỉĩịưứừửữựuúùủũụyýỳỷỹỵơớờởỡợoóòỏõọqQ]|gi|Gi)([uúùủũụ])([aimn]|ng|([ctp])|u)?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^aáàảãạoóòỏõọôốồổỗộơớờởỡợuúùủũụưứừửữự]|qu|Qu)([iíìỉĩị])([aumn]|nh|(ch|[ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^aáàảãạeéèẻẽẹoóòỏõọ])([oóòỏõọ])([imn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^u])([ôốồổỗộ])([imn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^uư]|qu|Qu)([ơớờởỡợ])([imn]|([tp]))?([@A@B@C@D@E])', funcs.tone ],
[ '()([ưứừửữự])([aiumn]|ng|([ct]))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^aáàảãạâấầẩẫậuúùủũụU]|qu|Qu)([yýỳỷỹỵ])(())?([@A@B@C@D@E])', funcs.tone ],
[ '()(i[êếềểễệ])([umn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '()([oóòỏõọ][aáàảãạ])([ioymn]|ng|nh|(ch|[ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '()(o[ăắằẳẵặ])([mn]|ng|([ct]))?([@A@B@C@D@E])', funcs.tone ],
[ '()([oóòỏõọ][eéèẻẽẹ])([omn]|(t))?([@A@B@C@D@E])', funcs.tone ],
[ '()(o[oóòỏõọ])(ng|(c))?([@A@B@C@D@E])', funcs.tone ],
[ '()(u[âấầẩẫậaáàảãạ])([yn]|ng|(t))?([@A@B@C@D@E])', funcs.tone ], // Bắt 'uâ' và 'ua'
[ '()(u[êếềểễệ])(nh|(ch))?([@A@B@C@D@E])', funcs.tone ],
[ '()(u[ôốồổỗộ])([imn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '()(u[ơớờởỡợ])(())?([@A@B@C@D@E])', funcs.tone ],
[ '()([uúùủũụU][yýỳỷỹỵ])([aun]|nh|(ch|[ctp]))?([@A@B@C@D@E])', funcs.tone ],
[ '()([ưƯuU][ơớờởỡợoóòỏõọ])([iumn]|ng|([ctp]))?([@A@B@C@D@E])', funcs.tone ], // "Ươc1", thêm tiền thân của "ươ" là "uo"
[ '()(uy[êếềểễệ])(n|(t))?([@A@B@C@D@E])', funcs.tone ],
[ '(^|[^u])(y[êếềểễệ])([umn]|ng|(t))?([@A@B@C@D@E])', funcs.tone ],
// B. CÁC PATTERN CHUYỂN VỊ TRÍ DẤU THANH
// Nhóm âm chính có dấu phụ: iê, oă, uâ, uê, uô, [uơ, ươ], uyê, yê
[ '()([íìỉĩị]e)()([@G])', funcs.moveTone ],
[ '()([óòỏõọ]a)()([@K])', funcs.moveTone$2 ],
[ '()([úùủũụ]a)()([@F])', funcs.moveTone ],
[ '()([úùủũụ]e)()([@G])', funcs.moveTone ],
[ '()([úùủũụ]o)()([@H])', funcs.moveTone ],
[ '()([úùủũụứừửữự]o)()([@I])', funcs.moveTone$1 ],
[ '()([úùủũụ]ye|u[ýỳỷỹỵ]e)()([@G])', funcs.moveTone ],
[ '()([ýỳỷỹỵ]e)()([@G])', funcs.moveTone ],
// Một số pattern có thể bỏ $3 do đã có nhóm ngay dưới thay thế.
// Nhóm âm chính có dấu phụ được hủy dấu phụ: ie, ua, ue, uo, ưo (*), uye, ye
[ '()([íìỉĩị]e)([umnctp])', funcs.moveTone$3 ],
[ '()([úùủũụ]a)([ynt])', funcs.moveTone$3 ],
[ '()([úùủũụ]e)([nc])', funcs.moveTone$3 ],
// "uo" đại diện cho "uô" và "ươ". Bắt uo, ưo, Ưo (thêm râu và chuyển dấu).
[ '()([úùủũụứừửữự]o)([iumnctp])', funcs.moveTone$3 ],
[ '()([úùủũụ]ye|u[ýỳỷỹỵ]e)([nt])', funcs.moveTone$3 ],
[ '()([ýỳỷỹỵ]e)([umnt])', funcs.moveTone$3 ],
// Nhóm âm chính không có dấu phụ: oa, oe, oo, uy (bỏ dấu kiểu mới nghiêm ngặt "?")
[ '()([óòỏõọ]a)([ioymnctp])' + ( oldStyle ? '' : '?' ), funcs.moveTone$3 ],
[ '()([óòỏõọ]e)([omnt])' + ( oldStyle ? '' : '?' ), funcs.moveTone$3 ],
[ '()([óòỏõọ]o)([nc])', funcs.moveTone$3 ],
[ '()([úùủũụ]y)([aunctp])' + ( oldStyle ? '' : '?' ), funcs.moveTone$3 ],
// Nhóm vần đặc biệt: gi + a/u/e/o
[ '([gG])([íìỉĩị][aueo])', funcs.moveTone$4 ],
// C. CÁC PATTERN GIEO DẤU PHỤ VÀ CHUYỂN ĐỔI DẤU PHỤ
// 1. Thêm dấu phụ mũ: â, ê, ô (9 âm chính có dấu phụ mũ)
// a, ă → â
[ '(^|[^u]|qu|Qu)([aáàảãạăắằẳẵặâấầẩẫậ])([uymnctp]|ng)?([@F])', funcs.mark ],
[ '(^|[^iu]|qu|gi|Qu|Gi)([eéèẻẽẹêếệềểễ])([umnctp]|nh|ch|ng)?([@G])', funcs.mark ],
// o, ơ → ô
[ '(^|[^uưUƯ])([oóòỏõọơớờởỡợôốộồổỗ])([imnctp]|ng)?([@H])', funcs.mark ],
[ '(u)([aáàảãạâấầẩẫậ])([ynt]|ng)?([@F])', funcs.mark ],
[ '(i)([eéèẻẽẹêếệềểễ])([umnctp]|ng)?([@G])', funcs.mark ],
[ '(u)([eéèẻẽẹêếệềểễ])(nh|ch)?([@G])', funcs.mark ],
[ '([uưUƯ])([oóòỏõọơớờởỡợôốộồổỗ])([imnctp]|ng)?([@H])', funcs.mark$del ], // Xử lý chuyển đổi "uô" <=> "ươ"
// ['(uy)([eéèẻẽẹ])([nt])?([@G])', ($0, $1, $2, $3, $4) => mark($0, $1, $2, $3, $4)], // Trùng cái ngay dưới
[ '(y)([eéèẻẽẹêếệềểễ])([umnt]|ng)?([@G])', funcs.mark ],
// 2. Thêm dấu phụ râu: ơ, ư (4 âm chính có dấu phụ râu: ơ, ư, uơ, ươ)
// o, ô → ơ
[ '(^|[^uUưƯ]|qu|Qu)([oóòỏõọôốồổỗộơớợờởỡ])([imntp])?([@I])', funcs.mark$1 ],
[ '(^|[^oóòỏõọ])([uúùủũụưứựừửữ])([aiumnct]|ng)?([@J])', funcs.mark$1 ], // ^o loại vần "ươu"
[ '%B([hH]u|[kK]hu|[tT]hu)([oóòỏõọôốồổỗộơớợờởỡ])()([@I])', funcs.mark$1 ], // huơ, thuở, khuơ
[ '([uUưƯ])([oóòỏõọôốồổỗộơớợờởỡ])([iumnctp]|ng)?([@I])', funcs.mark$add ],
[ '()([uU])([ơớờởỡợ][iumnctp])', funcs.mark$3 ], // "uơ_" → "ươ_" (huơ, thuở, khuơ)
[ '([ưƯ])([oóòỏõọ])([iumnctp])', funcs.mark$3 ], // "ưo_" → "ươ_" (sửa chính tả)
// 3. Thêm dấu phụ trăng: ă (2 âm chính có dấu phụ trăng: ă, oă)
// a, â → ă
[ '(^|[^o])([aáàảãạâấầẩẫậăắặằẳẵ])([mnctp]|ng)?([@K])', funcs.mark$2 ],
[ '(o)([aáàảãạăắặằẳẵ])([mnct]|ng)?([@K])', funcs.mark$2 ],
// 4. Thêm dấu phụ d ngang: đ
[ '([dDđĐ])(([iouưy]|uy)?([aáàảãạăắằẳẵặâấầẩẫậeéèẻẽẹêếềểễệiíìỉĩịoóòỏõọôốồổỗộơớờởỡợuúùủũụưứừửữựyýỳỷỹỵ])([aiouymnctp]|ng|nh|ch)?)?([@L])', funcs.stroke ],
// Xóa dấu thanh của từ
[ '([bdhlmnprstvxđgkc]|tr|[tcpnkg]h|gi|qu|ngh|ng)?([iouưy]|uy)?([áàảãạắằẳẵặấầẩẫậéèẻẽẹếềểễệíìỉĩịóòỏõọốồổỗộớờởỡợúùủũụứừửữựýỳỷỹỵ])([aiouymnctp]|ng|nh|ch)?[@M]', funcs.toneless ]
];
puncts = [
// Một số gõ tắt cho dấu câu, ký hiệu không có trên bàn phím.
[ '---', '–' ],
[ '<,>', '↕' ],
[ '<\\.>', '↔' ],
[ ',,>', '↑' ],
[ '<,,', '↓' ],
[ '\\.\\.>', '→' ],
[ '<\\.\\.', '←' ],
[ '§([^\\b\\B]{1,4})§ ', '$1' ]
];
patterns_shift = [
[ ' ', '§' ]
];
function preparePatterns( patternList ) {
var upperCasePatterns = [],
fullPatterns = [];
patternList.forEach( function ( pattern ) {
upperCasePatterns.push( [ pattern[ 0 ].toUpperCase(), pattern[ 1 ] ] );
} );
fullPatterns = patternList.concat( upperCasePatterns );
fullPatterns.forEach( function ( pattern, index, array ) {
array[ index ] = [ pattern[ 0 ].replace( /(@[A-M]|%[B])/g, function ( match ) {
return placeholders[ match ];
} ), pattern[ 1 ] ];
} );
console.log( fullPatterns );
return fullPatterns.concat( puncts );
}
vi = {
id: 'vi-vni-lint',
name: 'VNI Lint',
description: 'Gõ tiếng Việt - Kiểu VNI',
date: '2021-07-22',
URL: 'http://github.com/wikimedia/jquery.ime',
author: 'Plantaest',
license: 'MIT',
version: '1.0',
maxKeyLength: 5,
contextLength: 0,
patterns: preparePatterns( patterns ),
patterns_shift: patterns_shift
};
$.ime.register( vi );
}( jQuery ) );