PR
個人開発 カタカナハングル個人開発推し韓マル開発ノート発音言語処理韓国語学習

ハングルを「発音どおり」のカタカナに変換する仕組みを作った 文字どおりに置き換えると読みが間違う理由

ハングルが並んだ本のページ 個人開発
スポンサーリンク

文字どおりではなく、音のとおりに

日本語を話す学習者に、韓国語の読みをカタカナで見せるとき、文字をひとつずつ置き換えると間違った発音になります。

韓国語文字どおり(間違い)実際の発音(この変換)
감사합니다カムサハプニダカムサハムニダ(鼻音化)
맛있어요マッイッオヨマシッソヨ(連音)
같이カッイカチ(口蓋音化)
좋아요チョッアヨチョアヨ(ㅎ の脱落)
국물クッムルクンムル(鼻音化)
신라シンラシルラ(流音化)
밥 먹었어?パプ モゴッソ?パム モゴッソ?(分かち書きを越えた鼻音化)

(右の列は、この記事の変換を実際に動かした結果です。)

ハングルの学習アプリ「推し韓マル」の読みの表記と、Web「iffthen hangul」の読む画面で使っている変換の仕組みです。

構造:規則が先、カナは後

  1. 音節 → 初声・中声・終声に分ける(分かち書きと句読点は、境目の情報として記録)
  2. 標準の発音の規則を、決まった順番で当てる(字母の段階)
  3. 変わった字母 → カナ

規則と表記を混ぜないのが要です。「ㅂ パッチム + ㄴ = ㅁ」は字母の段階の規則で、「ㅁ パッチム = ム」は表記の段階の仕事です。

1)分けることと、境目

function parse(text) {
  const out = [];
  let boundary = 'phrase';                      // 文の頭
  for (const ch of text) {
    const code = ch.codePointAt(0) - 0xac00;
    if (code >= 0 && code < 11172) {
      out.push({ ch, cho: CHO[Math.floor(code / 588)], jung: JUNG[Math.floor((code % 588) / 28)],
                 jong: JONG[code % 28], boundary, after: '', tensedInWord: false });
      boundary = '';                            // 同じ語の中
    } else {
      const tail = PUNCT[ch] ?? ch;
      if (out.length > 0) out[out.length - 1].after += tail;
      if (/\s/.test(ch)) boundary = boundary !== 'phrase' ? 'word' : 'phrase';   // 分かち書き
      else if (ch in PUNCT) boundary = 'phrase';                                 // 句読点
    }
  }
  return out;
}

境目は 3 種類です。''(語の中)・'word'(分かち書きの後)・'phrase'(文の頭・句読点の後)。規則ごとに、越えられる境目が違います。

  • 連音・濃音化 = 語の中だけ
  • 鼻音化・流音化 = 分かち書きを越えても(続けて話すときの音・밥 먹었어 → パム モゴッソ)

2)規則を当てる順番(順番が結果を変える)

function applyRules(s) {
  // 1. ㅎ の規則 (激音化 · ㅎ の脱落 · 共鳴音の後の ㅎ の弱化)
  // 2. 連音 · 口蓋音化 (語の中)
  // 3. パッチムの代表音 (7 終声)
  // 4. 鼻音化 · 流音化 (分かち書きを越えて当てる)
  // 5. 濃音化 (語の中)
  // 6. 語の中の母音の後の濃音 → ッ で表す
}

たとえば 2 番の連音は、必ず 3 番の代表音より先に来なければなりません。「맛있어요」の ㅅ パッチムが代表音の ㄷ に変わる前に、次の音節へ移らないと、マシッソヨになりません。

連音・口蓋音化の部分です。

for (let i = 0; i < n - 1; i += 1) {
  const a = s[i], b = s[i + 1];
  if (b.boundary !== '' || b.cho !== 'ㅇ' || a.jong === '' || a.jong === 'ㅇ') continue;
  let moved;
  if (a.jong in SPLIT) {                         // 二重パッチム: 片方は残り、片方は移る (닭이 → 달기)
    const [remain, m] = SPLIT[a.jong];
    a.jong = remain;
    moved = m;
  } else {
    moved = a.jong;
    a.jong = '';
  }
  if ((moved === 'ㄷ' || moved === 'ㅌ') && b.jung === 'ㅣ') moved = moved === 'ㄷ' ? 'ㅈ' : 'ㅊ';   // 같이 → 가치
  if (moved === 'ㅎ') moved = 'ㅇ';
  b.cho = moved;
}

鼻音化・流音化の部分です。

for (let i = 0; i < n - 1; i += 1) {
  const a = s[i], b = s[i + 1];
  if (b.boundary === 'phrase') continue;                       // 文の境目は越えない
  if (b.cho === 'ㄹ' && (a.jong === 'ㅁ' || a.jong === 'ㅇ')) b.cho = 'ㄴ';
  if (b.cho === 'ㄹ' && (a.jong === 'ㄱ' || a.jong === 'ㅂ')) b.cho = 'ㄴ';
  if ((b.cho === 'ㄴ' || b.cho === 'ㅁ') && OBSTRUENT_FINAL.has(a.jong))
    a.jong = { ㄱ: 'ㅇ', ㄷ: 'ㄴ', ㅂ: 'ㅁ' }[a.jong];        // 합니다 → 함니다 · 국물 → 궁물
  if ((a.jong === 'ㄴ' && b.cho === 'ㄹ') || (a.jong === 'ㄹ' && b.cho === 'ㄴ')) {
    a.jong = 'ㄹ';                                              // 신라 → 실라
    b.cho = 'ㄹ';
  }
}

スポンサーリンク

モバイルアプリ開発の知識地図:一冊でiOS/Android/Flutterの開発技術が学べる本

モバイルアプリ開発の知識地図:一冊でiOS/Android/Flutterの開発技術が学べる本

楽天で見る

スポンサーリンク

モバイルアプリ開発の知識地図 / 國師誠也

モバイルアプリ開発の知識地図 / 國師誠也

Yahoo!ショッピングで見る

3)カナの表記:清音と濁音は「前の音」が決める

韓国語の平音 ㄱ ㄷ ㅂ ㅈ は、文の頭では無声音(カ タ パ チャ)、母音や共鳴音(ㄴ ㄹ ㅁ ㅇ)の後では有声音(ガ ダ バ ジャ)に聞こえます。

const prev = i > 0 ? s[i - 1] : null;
const voicedEnv = x.boundary !== 'phrase' && prev !== null && (prev.jong === '' || SONORANT_FINAL.has(prev.jong));
const [unvoiced, voiced] = ROW[x.cho];                 // ㄱ: ['k', 'g'] · ㄷ: ['t', 'd'] ...
const row = LENIS.has(x.cho) && voicedEnv ? voiced : unvoiced;
let kana = KANA[row][V[x.jung]];
if (x.tensedInWord) kana = `ッ${kana}`;               // 語の中の母音の後の濃音

パッチムは ㄱ = ク・ㄴ ㅇ = ン・ㄷ = ッ・ㄹ = ル・ㅁ = ム・ㅂ = プ で、ㄱ パッチム+ㄱ の系列・ㅂ パッチム+ㅂ の系列の前では ッ と書きます。

日本で定着している表記は、例外として合わせます。たとえば、ㄱ の系列+ㅙ ㅞ は ケ/ゲ と書きます(괜찮아 → ケンチャナ)。

音節ごとに分けた結果も一緒に返す

読む画面では、文字のマスごとにその音節の読みを付ける必要があります。同じ計算で、音節ごとに分けた結果を返します。前後の音の変化を反映したまま分かれます。

toRubyJaParts('감사합니다');
// [{ko:'감',kana:'カム'},{ko:'사',kana:'サ'},{ko:'합',kana:'ハム'},{ko:'니',kana:'ニ'},{ko:'다',kana:'ダ'}]

「합」は単独ならハプですが、ここでは後ろの「니」のためにハムになります。

Python の元と JS の移植版を、テストでつなぐ

この変換は、もともとコンテンツのビルド用に Python で作り、Web(ブラウザ・静的な生成)のために JS に移しました。2 つがずれると、アプリと Web の読みが違ってしまいます。

そこで、一致のテストを置いています。

  • 対象 1:コンテンツの元ファイルにあるすべての韓国語(単語・例文・表現)
  • 対象 2:正解の組の表
  • 対象 3:決定的な乱数で作った文(分かち書き・句読点を含む)

これらをすべて 2 つの実装に通し、ひとつでも違えば失敗にします。元を直したら移植版も一緒に直す、というのが決まりになります。

また、コンテンツの読みの欄は人が書きません。

  • --write:変換の結果で埋める
  • --check:元ファイルの読みが変換の結果と同じかを見る(違えば失敗・手で直した例外だけ許す)

限界(正直に)

  • サイシオッ、ㄴ の添加(색연필 → 생년필)、「알게 되다」のように ㄹ の後の -게 が濃音にならない場合は扱っていません。こうした語は、手で直した値を例外の一覧に置いています
  • 形態素解析をせず、「文字+境目」だけで計算しています。だから軽くて速いのですが、語の境目が分かち書きと違う合成語は間違えることがあります

スポンサーリンク

Coding Assistant対応 iPhoneアプリ開発集中講座 Xcode 26/iOS 26

Coding Assistant対応 iPhoneアプリ開発集中講座 Xcode 26/iOS 26

楽天で見る

スポンサーリンク

iPhoneアプリ開発集中講座 iOS 26 Xcode 26 AIアシスタント(Coding Assistant)も解説

iPhoneアプリ開発集中講座 iOS 26 Xcode 26 AIアシスタント(Coding Assistant)も解説

Yahoo!ショッピングで見る

この記事について

自社のハングル学習アプリ「推し韓マル」と、Web「iffthen hangul」の読みの変換を作ったときの記録です。コードは実際のソースから抜き出し、コメントを読みやすく縮めています。動作を変える修正はしていません。表の右の列は、この変換を実際に動かした結果です。

コメント

タイトルとURLをコピーしました