8
\$\begingroup\$

When my go-to Japanese transcription site went down for a while, I decided to write my own. My application converts between Romaji, Hiragana and Katakana — however, unlike any other converter I've seen, this one does a three-way conversion: there are three text boxes, and typing in one will update the content of the other two.

There's a working version here.

I'd like any feedback to focus on the big picture; that is, how I implemented this conversion. If anything else in my JS could be improved, though, don't hesitate to point that out as well.


How Japanese works

I figured I'd quickly introduce anyone who isn't familiar with Japanese to its writing systems. Keep in mind this is massively simplified, not least because I'm a beginner myself.

  • A Japanese character represents one syllable, which can either be a vowel, or a consonant followed by a vowel.
  • There are a few exceptions to how these combinations are transcribed: s + i is shi, t + i is chi, t + u is tsu, h + u is fu.
  • The only consonant that can appear without a vowel is n.
  • There are two different alphabets: Hiragana and Katakana. They both encode the same syllables, and they're virtually equivalent, they just use different-looking characters: hiragana are mostly round, katakana are blockier.
  • There's also Romaji, which is just representations of hiragana and katakana in the latin alphabet.
  • Example: the syllable me is written as め in hiragana and as メ in katakana. amerika in romaji is あめりか in hiragana and アメリカ in katakana.
  • A small tsu (っ or ッ) doubles the consonant that comes after it.
  • A small ya, yu or yo after a syllable ending in i combines the sounds (ki + small ya is kya).
  • ゙ or ゚ in the top right corner modify the consonant sound.
  • A ー doubles the vowel sound that comes before it. In Romaji, long vowels can also be written with a dash on top: ā is the same as aa.
  • Example: The Japanese word for "presentation" is happyōkai in Romaji, はっぴょうかい in hiragana and ハッピョウカイ in katakana.

How the converter works

I regard a string as split into "tokens", which is just the name I've given to a characters plus modifiers (like small tsu or small ya).

My conversion table is an array of objects, each representing a token and holding three strings for its representation in romaji, hiragana and katakana respectively.

The main function, convert, is given a text and the name of the writing system it's in. It loops through the text, cutting off the longest token it can find from the start of the string, and building the resulting strings from the contents of the token. It returns an object holding three strings, each representing the text in romaji, hiragana and katakana respectively.
That way, feeding in a string in one writing system simultaneously converts it into the other two.

When converting from romaji, spaces and the ' character are purposefully ignored. This is so that you can split syllables in words like bon'yari, to keep the syllable from being interpreted as nya.

The problem with this system is that it depends on the order of the tokens in the conversion table. If n came before a, it would first cut off n and then a, never recognizing na. I've found no way to work around this without using a different data structure entirely.


Converter.js

function Converter() {

    this.text = this.from = this.result = null;
    this.conversionTable = getConversionTable();

}


Converter.prototype.convert = function (text, from) {

    this.text = text.toLowerCase();
    this.from = from;

    this.result = {
        romajiText: '',
        hiraganaText: '',
        katakanaText: ''
    };

    this._preprocess();

    while (this.text !== '') {
        var token = this._getToken();
        this.result.romajiText += token.romaji;
        this.result.hiraganaText += token.hiragana;
        this.result.katakanaText += token.katakana;
        this.text = this.text.substr(token.strLength);
    }

    this._postprocess();

};


Converter.prototype.getResult = function() {

    return this.result;

};


Converter.prototype._preprocess = function () {

    this.text = this.text
        .replace(/ā/gi, 'aa')
        .replace(/ū/gi, 'uu')
        .replace(/ē/gi, 'ee')
        .replace(/ō/gi, 'ou');

};


Converter.prototype._getToken = function () {

    var newToken = {};

    if (this._shouldIgnoreChar(this.text[0])) {
        newToken.romaji = newToken.hiragana = newToken.katakana = '';
        newToken.strLength = 1;
        return newToken;
    }

    for (var i = 0; i < this.conversionTable.length; i++) {
        var token = this.conversionTable[i];
        if (this.text.startsWith(token[this.from])) {
            newToken = token;
            newToken.strLength = token[this.from].length;
            return newToken;
        }
    }

    newToken.romaji = newToken.hiragana = newToken.katakana = this.text[0];
    newToken.strLength = 1;
    return newToken;

};


Converter.prototype._shouldIgnoreChar = function (char) {

    return char === ' ' || char === '\'';

};


Converter.prototype._postprocess = function () {

    this.result.romajiText = this.result.romajiText
        .replace(/([aiueo])ー/gi, '$1$1')

        .replace(/aa/gi, 'ā')
        .replace(/uu/gi, 'ū')
        .replace(/ee/gi, 'ē')
        .replace(/ou/gi, 'ō')
        .replace(/oo/gi, 'ō');

};

conversionTable.js

function getConversionTable() {

    return [
        {romaji: 'kkya', hiragana: 'っきゃ', katakana: 'ッキャ'},
        {romaji: 'kkyu', hiragana: 'っきゅ', katakana: 'ッキュ'},
        {romaji: 'kkyo', hiragana: 'っきょ', katakana: 'ッキョ'},
        {romaji: 'ssha', hiragana: 'っしゃ', katakana: 'ッシャ'},
        {romaji: 'sshu', hiragana: 'っしゅ', katakana: 'ッシュ'},
        {romaji: 'ssho', hiragana: 'っしょ', katakana: 'ッショ'},
        {romaji: 'ccha', hiragana: 'っちゃ', katakana: 'ッチャ'},
        {romaji: 'cchu', hiragana: 'っちゅ', katakana: 'ッチュ'},
        {romaji: 'ccho', hiragana: 'っちょ', katakana: 'ッチョ'},
        {romaji: 'hhya', hiragana: 'っひゃ', katakana: 'ッヒャ'},
        {romaji: 'hhyu', hiragana: 'っひゅ', katakana: 'ッヒュ'},
        {romaji: 'hhyo', hiragana: 'っひょ', katakana: 'ッヒョ'},
        {romaji: 'mmya', hiragana: 'っみゃ', katakana: 'ッミャ'},
        {romaji: 'mmyu', hiragana: 'っみゅ', katakana: 'ッミュ'},
        {romaji: 'mmyo', hiragana: 'っみょ', katakana: 'ッミョ'},
        {romaji: 'rrya', hiragana: 'っりゃ', katakana: 'ッリャ'},
        {romaji: 'rryu', hiragana: 'っりゅ', katakana: 'ッリュ'},
        {romaji: 'rryo', hiragana: 'っりょ', katakana: 'ッリョ'},
        {romaji: 'ggya', hiragana: 'っぎゃ', katakana: 'ッギャ'},
        {romaji: 'ggyu', hiragana: 'っぎゅ', katakana: 'ッギュ'},
        {romaji: 'ggyo', hiragana: 'っぎょ', katakana: 'ッギョ'},
        {romaji: 'jja', hiragana: 'っじゃ', katakana: 'ッジャ'},
        {romaji: 'jju', hiragana: 'っじゅ', katakana: 'ッジュ'},
        {romaji: 'jjo', hiragana: 'っじょ', katakana: 'ッジョ'},
        {romaji: 'bbya', hiragana: 'っびゃ', katakana: 'ッビャ'},
        {romaji: 'bbyu', hiragana: 'っびゅ', katakana: 'ッビュ'},
        {romaji: 'bbyo', hiragana: 'っびょ', katakana: 'ッビョ'},
        {romaji: 'ppya', hiragana: 'っぴゃ', katakana: 'ッピャ'},
        {romaji: 'ppyu', hiragana: 'っぴゅ', katakana: 'ッピュ'},
        {romaji: 'ppyo', hiragana: 'っぴょ', katakana: 'ッピョ'},
        {romaji: 'yye', hiragana: 'っいぇ', katakana: 'ッイェ'},
        {romaji: 'wwi', hiragana: 'っわぃ', katakana: 'ッウィ'},
        {romaji: 'wwe', hiragana: 'っわぇ', katakana: 'ッウェ'},
        {romaji: 'wwo', hiragana: 'っわぉ', katakana: 'ッウォ'},
        {romaji: 'vva', hiragana: 'っゔぁ', katakana: 'ッヴァ'},
        {romaji: 'vvi', hiragana: 'っゔぃ', katakana: 'ッヴィ'},
        {romaji: 'vve', hiragana: 'っゔぇ', katakana: 'ッヴェ'},
        {romaji: 'vvo', hiragana: 'っゔぉ', katakana: 'ッヴォ'},
        {romaji: 'ssi', hiragana: 'っすぃ', katakana: 'ッスィ'},
        {romaji: 'zzi', hiragana: 'っずぃ', katakana: 'ッズィ'},
        {romaji: 'sshe', hiragana: 'っしぇ', katakana: 'ッシェ'},
        {romaji: 'jje', hiragana: 'っじぇ', katakana: 'ッジェ'},
        {romaji: 'tti', hiragana: 'っとぃ', katakana: 'ッティ'},
        {romaji: 'ttu', hiragana: 'っとぅ', katakana: 'ットゥ'},
        {romaji: 'ddi', hiragana: 'っでぅ', katakana: 'ッディ'},
        {romaji: 'ddu', hiragana: 'っどぅ', katakana: 'ッドゥ'},
        {romaji: 'ttsa', hiragana: 'っつぁ', katakana: 'ッツァ'},
        {romaji: 'ttsi', hiragana: 'っつぃ', katakana: 'ッツィ'},
        {romaji: 'ttse', hiragana: 'っつぇ', katakana: 'ッツェ'},
        {romaji: 'ttso', hiragana: 'っつぉ', katakana: 'ッツォ'},
        {romaji: 'ffa', hiragana: 'っふぁ', katakana: 'ッファ'},
        {romaji: 'ffi', hiragana: 'っふぃ', katakana: 'ッフィ'},
        {romaji: 'ffe', hiragana: 'っふぇ', katakana: 'ッフェ'},
        {romaji: 'ffo', hiragana: 'っふぉ', katakana: 'ッフォ'},
        {romaji: 'ffyu', hiragana: 'っふゅ', katakana: 'ッフュ'},
        {romaji: 'hhye', hiragana: 'っひぇ', katakana: 'ッヒェ'},
        {romaji: 'kya', hiragana: 'きゃ', katakana: 'キャ'},
        {romaji: 'kyu', hiragana: 'きゅ', katakana: 'キュ'},
        {romaji: 'kyo', hiragana: 'きょ', katakana: 'キョ'},
        {romaji: 'sha', hiragana: 'しゃ', katakana: 'シャ'},
        {romaji: 'shu', hiragana: 'しゅ', katakana: 'シュ'},
        {romaji: 'sho', hiragana: 'しょ', katakana: 'ショ'},
        {romaji: 'cha', hiragana: 'ちゃ', katakana: 'チャ'},
        {romaji: 'chu', hiragana: 'ちゅ', katakana: 'チュ'},
        {romaji: 'cho', hiragana: 'ちょ', katakana: 'チョ'},
        {romaji: 'nya', hiragana: 'にゃ', katakana: 'ニャ'},
        {romaji: 'nyu', hiragana: 'にゅ', katakana: 'ニュ'},
        {romaji: 'nyo', hiragana: 'にょ', katakana: 'ニョ'},
        {romaji: 'hya', hiragana: 'ひゃ', katakana: 'ヒャ'},
        {romaji: 'hyu', hiragana: 'ひゅ', katakana: 'ヒュ'},
        {romaji: 'hyo', hiragana: 'ひょ', katakana: 'ヒョ'},
        {romaji: 'mya', hiragana: 'みゃ', katakana: 'ミャ'},
        {romaji: 'myu', hiragana: 'みゅ', katakana: 'ミュ'},
        {romaji: 'myo', hiragana: 'みょ', katakana: 'ミョ'},
        {romaji: 'rya', hiragana: 'りゃ', katakana: 'リャ'},
        {romaji: 'ryu', hiragana: 'りゅ', katakana: 'リュ'},
        {romaji: 'ryo', hiragana: 'りょ', katakana: 'リョ'},
        {romaji: 'gya', hiragana: 'ぎゃ', katakana: 'ギャ'},
        {romaji: 'gyu', hiragana: 'ぎゅ', katakana: 'ギュ'},
        {romaji: 'gyo', hiragana: 'ぎょ', katakana: 'ギョ'},
        {romaji: 'ja', hiragana: 'じゃ', katakana: 'ジャ'},
        {romaji: 'ju', hiragana: 'じゅ', katakana: 'ジュ'},
        {romaji: 'jo', hiragana: 'じょ', katakana: 'ジョ'},
        {romaji: 'bya', hiragana: 'びゃ', katakana: 'ビャ'},
        {romaji: 'byu', hiragana: 'びゅ', katakana: 'ビュ'},
        {romaji: 'byo', hiragana: 'びょ', katakana: 'ビョ'},
        {romaji: 'pya', hiragana: 'ぴゃ', katakana: 'ピャ'},
        {romaji: 'pyu', hiragana: 'ぴゅ', katakana: 'ピュ'},
        {romaji: 'pyo', hiragana: 'ぴょ', katakana: 'ピョ'},
        {romaji: 'ye', hiragana: 'いぇ', katakana: 'イェ'},
        {romaji: 'wi', hiragana: 'わぃ', katakana: 'ウィ'},
        {romaji: 'we', hiragana: 'わぇ', katakana: 'ウェ'},
        {romaji: 'wo', hiragana: 'わぉ', katakana: 'ウォ'},
        {romaji: 'va', hiragana: 'ゔぁ', katakana: 'ヴァ'},
        {romaji: 'vi', hiragana: 'ゔぃ', katakana: 'ヴィ'},
        {romaji: 've', hiragana: 'ゔぇ', katakana: 'ヴェ'},
        {romaji: 'vo', hiragana: 'ゔぉ', katakana: 'ヴォ'},
        {romaji: 'si', hiragana: 'すぃ', katakana: 'スィ'},
        {romaji: 'zi', hiragana: 'ずぃ', katakana: 'ズィ'},
        {romaji: 'she', hiragana: 'しぇ', katakana: 'シェ'},
        {romaji: 'je', hiragana: 'じぇ', katakana: 'ジェ'},
        {romaji: 'ti', hiragana: 'とぃ', katakana: 'ティ'},
        {romaji: 'tu', hiragana: 'とぅ', katakana: 'トゥ'},
        {romaji: 'di', hiragana: 'でぅ', katakana: 'ディ'},
        {romaji: 'du', hiragana: 'どぅ', katakana: 'ドゥ'},
        {romaji: 'tsa', hiragana: 'つぁ', katakana: 'ツァ'},
        {romaji: 'tsi', hiragana: 'つぃ', katakana: 'ツィ'},
        {romaji: 'tse', hiragana: 'つぇ', katakana: 'ツェ'},
        {romaji: 'tso', hiragana: 'つぉ', katakana: 'ツォ'},
        {romaji: 'fa', hiragana: 'ふぁ', katakana: 'ファ'},
        {romaji: 'fi', hiragana: 'ふぃ', katakana: 'フィ'},
        {romaji: 'fe', hiragana: 'ふぇ', katakana: 'フェ'},
        {romaji: 'fo', hiragana: 'ふぉ', katakana: 'フォ'},
        {romaji: 'fyu', hiragana: 'ふゅ', katakana: 'フュ'},
        {romaji: 'hye', hiragana: 'ひぇ', katakana: 'ヒェ'},
        {romaji: 'kka', hiragana: 'っか', katakana: 'ッカ'},
        {romaji: 'kki', hiragana: 'っき', katakana: 'ッキ'},
        {romaji: 'kku', hiragana: 'っく', katakana: 'ック'},
        {romaji: 'kke', hiragana: 'っけ', katakana: 'ッケ'},
        {romaji: 'kko', hiragana: 'っこ', katakana: 'ッコ'},
        {romaji: 'ssa', hiragana: 'っさ', katakana: 'ッサ'},
        {romaji: 'sshi', hiragana: 'っし', katakana: 'ッシ'},
        {romaji: 'ssu', hiragana: 'っす', katakana: 'ッス'},
        {romaji: 'sse', hiragana: 'っせ', katakana: 'ッセ'},
        {romaji: 'sso', hiragana: 'っそ', katakana: 'ッソ'},
        {romaji: 'tta', hiragana: 'った', katakana: 'ッタ'},
        {romaji: 'cchi', hiragana: 'っち', katakana: 'ッチ'},
        {romaji: 'ttsu', hiragana: 'っつ', katakana: 'ッツ'},
        {romaji: 'tte', hiragana: 'って', katakana: 'ッテ'},
        {romaji: 'tto', hiragana: 'っと', katakana: 'ット'},
        {romaji: 'hha', hiragana: 'っは', katakana: 'ッハ'},
        {romaji: 'hhi', hiragana: 'っひ', katakana: 'ッヒ'},
        {romaji: 'ffu', hiragana: 'っふ', katakana: 'ッフ'},
        {romaji: 'hhe', hiragana: 'っへ', katakana: 'ッヘ'},
        {romaji: 'hho', hiragana: 'っほ', katakana: 'ッホ'},
        {romaji: 'mma', hiragana: 'っま', katakana: 'ッマ'},
        {romaji: 'mmi', hiragana: 'っみ', katakana: 'ッミ'},
        {romaji: 'mmu', hiragana: 'っむ', katakana: 'ッム'},
        {romaji: 'mme', hiragana: 'っめ', katakana: 'ッメ'},
        {romaji: 'mmo', hiragana: 'っも', katakana: 'ッモ'},
        {romaji: 'yya', hiragana: 'っや', katakana: 'ッヤ'},
        {romaji: 'yyu', hiragana: 'っゆ', katakana: 'ッユ'},
        {romaji: 'yyo', hiragana: 'っよ', katakana: 'ッヨ'},
        {romaji: 'rra', hiragana: 'っら', katakana: 'ッラ'},
        {romaji: 'rri', hiragana: 'っり', katakana: 'ッリ'},
        {romaji: 'rru', hiragana: 'っる', katakana: 'ッル'},
        {romaji: 'rre', hiragana: 'っれ', katakana: 'ッレ'},
        {romaji: 'rro', hiragana: 'っろ', katakana: 'ッロ'},
        {romaji: 'wwa', hiragana: 'っわ', katakana: 'ッワ'},
        {romaji: 'wwi', hiragana: 'っゐ', katakana: 'ッヰ'},
        {romaji: 'wwe', hiragana: 'っゑ', katakana: 'ッヱ'},
        {romaji: 'wwo', hiragana: 'っを', katakana: 'ッヲ'},
        {romaji: 'gga', hiragana: 'っが', katakana: 'ッガ'},
        {romaji: 'ggi', hiragana: 'っぎ', katakana: 'ッギ'},
        {romaji: 'ggu', hiragana: 'っぐ', katakana: 'ッグ'},
        {romaji: 'gge', hiragana: 'っげ', katakana: 'ッゲ'},
        {romaji: 'ggo', hiragana: 'っご', katakana: 'ッゴ'},
        {romaji: 'zza', hiragana: 'っざ', katakana: 'ッザ'},
        {romaji: 'jji', hiragana: 'っじ', katakana: 'ッジ'},
        {romaji: 'zzu', hiragana: 'っず', katakana: 'ッズ'},
        {romaji: 'zze', hiragana: 'っぜ', katakana: 'ッゼ'},
        {romaji: 'zzo', hiragana: 'っぞ', katakana: 'ッゾ'},
        {romaji: 'dda', hiragana: 'っだ', katakana: 'ッダ'},
        {romaji: 'jji', hiragana: 'っぢ', katakana: 'ッヂ'},
        {romaji: 'ddzu', hiragana: 'っづ', katakana: 'ッヅ'},
        {romaji: 'dde', hiragana: 'っで', katakana: 'ッデ'},
        {romaji: 'ddo', hiragana: 'っど', katakana: 'ッド'},
        {romaji: 'bba', hiragana: 'っば', katakana: 'ッバ'},
        {romaji: 'bbi', hiragana: 'っび', katakana: 'ッビ'},
        {romaji: 'bbu', hiragana: 'っぶ', katakana: 'ッブ'},
        {romaji: 'bbe', hiragana: 'っべ', katakana: 'ッベ'},
        {romaji: 'bbo', hiragana: 'っぼ', katakana: 'ッボ'},
        {romaji: 'ppa', hiragana: 'っぱ', katakana: 'ッパ'},
        {romaji: 'ppi', hiragana: 'っぴ', katakana: 'ッパ'},
        {romaji: 'ppu', hiragana: 'っぷ', katakana: 'ップ'},
        {romaji: 'ppe', hiragana: 'っぺ', katakana: 'ッペ'},
        {romaji: 'ppo', hiragana: 'っぽ', katakana: 'ッポ'},
        {romaji: 'vvu', hiragana: 'っゔ', katakana: 'ッヴ'},
        {romaji: 'a', hiragana: 'あ', katakana: 'ア'},
        {romaji: 'i', hiragana: 'い', katakana: 'イ'},
        {romaji: 'u', hiragana: 'う', katakana: 'ウ'},
        {romaji: 'e', hiragana: 'え', katakana: 'エ'},
        {romaji: 'o', hiragana: 'お', katakana: 'オ'},
        {romaji: 'ka', hiragana: 'か', katakana: 'カ'},
        {romaji: 'ki', hiragana: 'き', katakana: 'キ'},
        {romaji: 'ku', hiragana: 'く', katakana: 'ク'},
        {romaji: 'ke', hiragana: 'け', katakana: 'ケ'},
        {romaji: 'ko', hiragana: 'こ', katakana: 'コ'},
        {romaji: 'sa', hiragana: 'さ', katakana: 'サ'},
        {romaji: 'shi', hiragana: 'し', katakana: 'シ'},
        {romaji: 'su', hiragana: 'す', katakana: 'ス'},
        {romaji: 'se', hiragana: 'せ', katakana: 'セ'},
        {romaji: 'so', hiragana: 'そ', katakana: 'ソ'},
        {romaji: 'ta', hiragana: 'た', katakana: 'タ'},
        {romaji: 'chi', hiragana: 'ち', katakana: 'チ'},
        {romaji: 'tsu', hiragana: 'つ', katakana: 'ツ'},
        {romaji: 'te', hiragana: 'て', katakana: 'テ'},
        {romaji: 'to', hiragana: 'と', katakana: 'ト'},
        {romaji: 'na', hiragana: 'な', katakana: 'ナ'},
        {romaji: 'ni', hiragana: 'に', katakana: 'ニ'},
        {romaji: 'nu', hiragana: 'ぬ', katakana: 'ヌ'},
        {romaji: 'ne', hiragana: 'ね', katakana: 'ネ'},
        {romaji: 'no', hiragana: 'の', katakana: 'ノ'},
        {romaji: 'ha', hiragana: 'は', katakana: 'ハ'},
        {romaji: 'hi', hiragana: 'ひ', katakana: 'ヒ'},
        {romaji: 'fu', hiragana: 'ふ', katakana: 'フ'},
        {romaji: 'he', hiragana: 'へ', katakana: 'ヘ'},
        {romaji: 'ho', hiragana: 'ほ', katakana: 'ホ'},
        {romaji: 'ma', hiragana: 'ま', katakana: 'マ'},
        {romaji: 'mi', hiragana: 'み', katakana: 'ミ'},
        {romaji: 'mu', hiragana: 'む', katakana: 'ム'},
        {romaji: 'me', hiragana: 'め', katakana: 'メ'},
        {romaji: 'mo', hiragana: 'も', katakana: 'モ'},
        {romaji: 'ya', hiragana: 'や', katakana: 'ヤ'},
        {romaji: 'yu', hiragana: 'ゆ', katakana: 'ユ'},
        {romaji: 'yo', hiragana: 'よ', katakana: 'ヨ'},
        {romaji: 'ra', hiragana: 'ら', katakana: 'ラ'},
        {romaji: 'ri', hiragana: 'り', katakana: 'リ'},
        {romaji: 'ru', hiragana: 'る', katakana: 'ル'},
        {romaji: 're', hiragana: 'れ', katakana: 'レ'},
        {romaji: 'ro', hiragana: 'ろ', katakana: 'ロ'},
        {romaji: 'wa', hiragana: 'わ', katakana: 'ワ'},
        {romaji: 'wi', hiragana: 'ゐ', katakana: 'ヰ'},
        {romaji: 'we', hiragana: 'ゑ', katakana: 'ヱ'},
        {romaji: 'wo', hiragana: 'を', katakana: 'ヲ'},
        {romaji: 'n', hiragana: 'ん', katakana: 'ン'},
        {romaji: 'ga', hiragana: 'が', katakana: 'ガ'},
        {romaji: 'gi', hiragana: 'ぎ', katakana: 'ギ'},
        {romaji: 'gu', hiragana: 'ぐ', katakana: 'グ'},
        {romaji: 'ge', hiragana: 'げ', katakana: 'ゲ'},
        {romaji: 'go', hiragana: 'ご', katakana: 'ゴ'},
        {romaji: 'za', hiragana: 'ざ', katakana: 'ザ'},
        {romaji: 'ji', hiragana: 'じ', katakana: 'ジ'},
        {romaji: 'zu', hiragana: 'ず', katakana: 'ズ'},
        {romaji: 'ze', hiragana: 'ぜ', katakana: 'ゼ'},
        {romaji: 'zo', hiragana: 'ぞ', katakana: 'ゾ'},
        {romaji: 'da', hiragana: 'だ', katakana: 'ダ'},
        {romaji: 'ji', hiragana: 'ぢ', katakana: 'ヂ'},
        {romaji: 'dzu', hiragana: 'づ', katakana: 'ヅ'},
        {romaji: 'de', hiragana: 'で', katakana: 'デ'},
        {romaji: 'do', hiragana: 'ど', katakana: 'ド'},
        {romaji: 'ba', hiragana: 'ば', katakana: 'バ'},
        {romaji: 'bi', hiragana: 'び', katakana: 'ビ'},
        {romaji: 'bu', hiragana: 'ぶ', katakana: 'ブ'},
        {romaji: 'be', hiragana: 'べ', katakana: 'ベ'},
        {romaji: 'bo', hiragana: 'ぼ', katakana: 'ボ'},
        {romaji: 'pa', hiragana: 'ぱ', katakana: 'パ'},
        {romaji: 'pi', hiragana: 'ぴ', katakana: 'パ'},
        {romaji: 'pu', hiragana: 'ぷ', katakana: 'プ'},
        {romaji: 'pe', hiragana: 'ぺ', katakana: 'ペ'},
        {romaji: 'po', hiragana: 'ぽ', katakana: 'ポ'},
        {romaji: 'vu', hiragana: 'ゔ', katakana: 'ヴ'},
        {romaji: ',', hiragana: '、', katakana: '、'},
        {romaji: '.', hiragana: '。', katakana: '。'}
    ];

}

sampleUsage.html

<meta charset="UTF-8" />

<textarea id="romaji"></textarea>
<textarea id="hiragana"></textarea>
<textarea id="katakana"></textarea>

<script type="text/javascript" src="startswith.js"></script> <!-- https://github.com/mathiasbynens/String.prototype.startsWith/blob/master/startswith.js -->
<script type="text/javascript" src="conversionTable.js"></script>
<script type="text/javascript" src="Converter.js"></script>

<script type="text/javascript">

    var romajiInput = document.getElementById('romaji');
    var hiraganaInput = document.getElementById('hiragana');
    var katakanaInput = document.getElementById('katakana');

    var converter = new Converter();

    romajiInput.onkeyup = hiraganaInput.onkeyup = katakanaInput.onkeyup = function () {

        var from = this.id;

        converter.convert(this.value, from);
        var conversionResult = converter.getResult();

        if (this !== romajiInput) {
            romajiInput.value = conversionResult.romajiText;
        }
        if (this !== hiraganaInput) {
            hiraganaInput.value = conversionResult.hiraganaText;
        }
        if (this !== katakanaInput) {
            katakanaInput.value = conversionResult.katakanaText;
        }

    };

</script>
\$\endgroup\$
  • \$\begingroup\$ When a website goes down, I suggest looking at The Internet Archive or Google cache for a cached copy. It's handy when a site is downed, and if permanently, still see their implementation (if it's just in JS). \$\endgroup\$ – Joseph Nov 22 '15 at 14:06
  • \$\begingroup\$ Something you could improve on is the extraneous adding of the small tsus to the double consonant characters, instead append it. \$\endgroup\$ – Quill Nov 22 '15 at 16:29
3
\$\begingroup\$

This is so cool! I may finally realize the dream of learning Japanese :D

Anyways, back to your code.

var romajiInput = document.getElementById('romaji');
var hiraganaInput = document.getElementById('hiragana');
var katakanaInput = document.getElementById('katakana');

var converter = new Converter();

romajiInput.onkeyup = hiraganaInput.onkeyup = katakanaInput.onkeyup = function () {

    var from = this.id;

    converter.convert(this.value, from);
    var conversionResult = converter.getResult();

    if (this !== romajiInput) {
        romajiInput.value = conversionResult.romajiText;
    }
    if (this !== hiraganaInput) {
        hiraganaInput.value = conversionResult.hiraganaText;
    }
    if (this !== katakanaInput) {
        katakanaInput.value = conversionResult.katakanaText;
    }

};

This is cool, nothing wrong about it. But if you're considering, try using a framework that supports basic two-way binding. That way, you don't have to deal with syncing the DOM with your data. Here's an example using Ractive.js

var JapaneseConversionWidget = Ractive.extend({
  // If you have the luxury of ES6, you can use template strings
  template: `
    <textarea value="{{ hiragana }}" on-change="fromHiragana(hiragana)"></textarea>
    <textarea value="{{ katakana }}" on-change="fromKatakana(katakana)"></textarea>
    <textarea value="{{ romaji }}" on-change="fromRomaji(romaji)"></textarea>
  `,
  // This autobinds to the DOM
  data: {
    hiragana: '',
    katakana: '',
    romaji: '',
  },
  // Assuming convert returns an object like {hiragana: '', katakana: '', romaji: ''}
  // Now all I'm doing is `set. The library does everything else for me.
  fromHiragana: function(text){
    this.set(convertFromHiragana(text))
  },
  fronKatakana: function(text){
    this.set(convertFromKatakana(text))
  },
  fromRomaji: function(text){
    this.set(convertFromRomaji(text))
  }
});

new JapaneseConversionWidget({
  el: document.body,
  append: true
});

Another thing is that its better if you split your convert into more distinct operations. In the sample framework code shown above, I explicitly created functions for conversion from Hiragana, Katakana and Romaji. This prevents your convert function from becoming bloated, especially when you add dialect-specific parsing routines.

As for your converter, I don't think you really need to use prototypes for it although there's nothing wrong with doing so either. It's just that you're not doing inheritance, and the same feat can be done with just a series of transformation functions.

Now usually I'd do things in a "functional" way (not really a follower of the paradigm, but know enough to get the benefits). I suggest you create your functions transparently. That means given the same input, the function should always give the same output, regardless of what's happening on the outside, specifically the implicit mutations of properties on this.

// convert
while (this.text !== '') {
    var token = this._getToken();
    this.result.romajiText += token.romaji;
    this.result.hiraganaText += token.hiragana;
    this.result.katakanaText += token.katakana;
    this.text = this.text.substr(token.strLength);
}

The one problem I see is the use of a loop. It gives me the scares, and the fear that this will be an infinite loop eventually. What I would suggest is to have a function that accepts a string, and returns an array of tokens instead. That way, you have a finite set to operate with and easily used by array methods like map, reduce etc.

function tokenizeRomaji(text){
  return text.split('').reduce(function(syllables, character){
    // Logic to group individual characters to syllables.
    // For Romaji, you can add Romaji-specific routines
  }, [])
}

function mapTokensToCharacters(tokens){
  return tokens.map(function(token){
    return //Convert token into another dialect
  });
}

What I suggest is doing something like this:

function convertFromHiragana(text){

  var lowerCasedText = text.toLowerCase();
  var preprocessedText = preprocess(lowerCasedText);

  // Instead of "running with" in getting tokens, why not create an array of
  // tokens instead, then hand it off to individual translators? This also
  // makes the tokenizer dialect-specific. This means that even if your table
  // is shared, dialect-specific quirks can be worked-around.
  var tokenizedText = tokenizeHiragana(preprocessedText);

  // Explicitly separating translators. Since we come from Hiragana, we don't
  // translate Hiragana.
  var katakanaTranslation = convertToKatakana(tokenizedText);
  var romajiTranslation = convertToRomaji(tokenizedText);

  // Return as object. Note that we explicitly postProcess Romaji instead of
  // blindly calling postProcess and making it an implicit Romaji-only operation.
  return {
    hiragana: text,
    katakana: katakanaTranslation,
    romaji: postProcess(romajiTranslation)
  }
}

Sure, there's a lot of typing here, and more explicitness of code. However, we know that tokenizeHiragana does just tokenizing a Hiragana string into an array of tokens. We know we come from Hiragana, thus avoid Hiragana conversion. We know that the convert* functions can operate independently.

With the above approach, you can have dialect-specific tokenizers. For instance, your Romaji tokenizer can "look ahead" to see if there's a vowel after the current token and merge it, or "look behind" to see if a vowel is preceded by an n and merge it.

I wouldn't worry about repetetive code or being DRY at the moment. I'd worry much about making functions independent, and that bugs implemented for one operation doesn't affect another operation. You can start trimming off code at a later stage once you have the entire thing running perfectly and have tests. One cause of regression is refactoring without tests.

\$\endgroup\$
  • \$\begingroup\$ Thanks a ton for the in-depth feedback! It's a lot to take in, but I think I can learn quite a bit from it. :) I'll wait just a bit longer before accepting your answer. \$\endgroup\$ – vvye Nov 23 '15 at 10:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.