コンテンツにスキップ

英語学習アプリ

TOEIC対策向けの英語学習アプリです。学ぶ単位を、英文・意味句・イントネーション・ユニット(IU)・単語の4つにそろえ、表のように選んで、1枚のカードで学びます。以前に作った試作のアプリ(公開は終了しました)を元に、設計から作り直しました。英文は端末に保存し、クラウド同期を使うときは端末の中で暗号化してから送ります。

このページは、その仕組みと、作るうえで判断したことの解説です。

試作から、何を変えたか

試作は、機能ごとに画面が並んでいました。新しいアプリは、使う順に「登録 → 学習対象選択 → 学習」の3段にしました。構成は、スプレッドシートに、機能を行として書き出して考えました。

試作 新しいアプリ
学ぶ単位 英文と単語 英文・意味句・IU・単語
画面 機能ごと(モード) 登録 → 選択 → 学習
選び方 一覧から1文ずつ 表で、チェック・ランダム・グループ・絞り込み
保存先 localStorage(約5MB) IndexedDB
発音記号 単語だけ 単語・意味句・IU

4つの単位

単位 中身 区切りの基準
英文 1文 —
意味句 英文を、意味のまとまりで区切ったもの(和訳つき) 意味
IU 英文を、息継ぎ・音の区切りで区切ったもの 音
単語 単語・熟語(意味・品詞・頻出度・例文つき) —

意味句とIUは、どちらも英文を前から区切りますが、区切る場所は同じとは限りません。そこで、区切りは、それぞれ別に持ちます。ただし、どちらも、つなぐと原文と一致するものだけを受け付けます。

function iu(list, text) {
  // …各区切りの t を確かめる
  return norm(out.map(x => x.t).join(" ")) === norm(text) ? out : null;
}

英文の表に、意味句(slash)とIU(iu)を持たせ、「英文から取り出す」で、意味句・IU・単語の表を作ります。難易度とグループは、その英文のものを引き継ぎます。

登録

登録の経路は、4つあります。

  • サンプル:オリジナルの英文90文を、解析つきで入れます。APIは要りません。
  • 貼り付け・ファイル:.csv・.txt・.json。同じ英文は、足りない項目だけを足します(解析・状態・記録は上書きしません)。
  • AI(Gemini):和訳・意味句・区切りの形・品詞・IU・単語・文法・TOEICの観点・4択を、まとめて付けます。方法は2つ。Gemini APIは、1文ずつ自動で呼びます(止められる。エラーで中断)。コピペモードは、APIキーが要りません。プロンプトを Gemini に貼り、返事を貼り戻します(5文ずつ)。
  • 受信箱:Claude Code などが、英文を預けます(後述)。

AIの返事は、そのままでは入れません。区切りをつなぐと原文と一致するか、品詞コードが正しいか、4択の形が正しいか、を検査し、通らない部分だけ見送って、ほかは入れます。入れた英文は「未確認」になります。APIキーは、この端末のブラウザの中だけに保存し、バックアップや同期には入れません。

samples のIUは、位置の番号だけを答えさせて作った

samples の90文には、IUも先に入れました。ここでも、AIに文字を書き写させると、ずれやすいので、語を並べた表を渡し、「何番目から何番目まで」だけを答えさせました。機械で確かめたのは、すき間も重なりもなく全部の語を覆っていること、1文あたり1〜6個であること、1つが10語以内であること、です。90文とも通りました。249個のIUの語数は、4〜7語が中心です。

発音記号(IPA)

発音記号は、AIに書かせず、発音辞書から作ります。AIに任せると、もっともらしい誤りが混じるためです。辞書は、CMU Pronouncing Dictionary(Carnegie Mellon University が作った、アメリカ英語の発音辞書)を使います。

どこに付くか

  • samples:単語・意味句・IUのすべてに、辞書から作ったものを、あらかじめ入れてあります。
  • AIで解析した英文:意味句・IU・新しく入る単語に、辞書から付けます。AIが付けた発音記号は、辞書で作れる語については、辞書のものに置き換えます。辞書に無い語(résumé など)は、AIのものを残します。
  • 貼り付けた単語・すでに入っているもの:単語を貼り付けると、自動で付きます。「登録」の「発音記号を辞書から付ける」で、発音記号が無い単語・意味句・IUに、まとめて付けられます(すでにあるものは、そのままです)。

辞書の作り方

辞書の音は、ARPABETという記号(AH0・K など)で書かれています。tools/build-pron.mjs が、これをIPAに直して、アプリが読む public/dict/cmudict-ipa.txt(1行に「単語|IPA」)を作ります。

  • 強勢の印を、音節の頭に置く:辞書は、強勢を母音に付けます。IPAでは、強勢の印(ˈ ˌ)は音節の頭に置くので、母音の前の子音のまとまりのうち、音節の頭になれる並び(pl・str など)を選んで、その前に置きます(kəmˈplit、ˈstɹætədʒi)。
  • 載せるのは、英字・アポストロフィ・ハイフンだけの語(約12.6万語)です。
  • ファイルは約2.7MBですが、配信のときに圧縮されるので、通信量は約0.8MBです。必要になったときに、1回だけ読み込みます。発音記号がそろっているサンプルだけを使うときは、読み込みません。通信できないときは、何もせず、あとで付けられます。

区切りの発音記号

意味句・IUの発音記号は、語ごとの辞書の発音をつないだものです。

  • of・to・the などの機能語は、弱い発音(əv・tə・ðə)にします。
  • 's は、前の語の音に z を付けます。ハイフンでつないだ語は、辞書にあればその発音を、なければ前後に分けてつなぎます。
  • 1語でも辞書に無ければ、その区切りには付けません(途中が抜けた発音記号を出さないためです)。数字・固有名詞などを含む区切りは、付かないことがあります。
  • 文全体での、弱い発音や音のつながりは出ません。

確かめたこと

samples の意味句・IU・単語、976件について、アプリが辞書から作る発音記号を、samples にあらかじめ入れてあるものと比べました。辞書で作れた963件はすべて一致し(相違0件)、残りの13件は、辞書に無い語(résumé・intranet・数字など)を含む区切りでした。

作るなかで直したこと

  • 辞書の注釈:辞書の一部の値に、# … という注釈が付いていて、変換でエラーになりました。注釈を取り除いてから変換するようにしました。
  • ハイフンの語:最初は、ハイフンを含む語を辞書に載せていませんでした。そのため、full-time を、full と time に分けて、それぞれに強い強勢(ˈ)が付きました。辞書には、full-time の発音(強い強勢と、弱い強勢が1つずつ)があるので、ハイフンの語も載せるようにして、一致しました。

出典:CMU Pronouncing Dictionary。使用条件は、元の辞書の配布元の表示に従います(public/dict/NOTICE.txt)。

学習対象選択

どの単位でも、同じ3つの選び方です。

  1. チェックボックス:表で選ぶ。並べ替え・絞り込み・全選択ができる
  2. ランダム:絞り込んだ範囲から、N件
  3. グループ:グループで絞る

絞り込みは、グループ・状態・難易度に加えて、意味句とIUでは、語数(1〜2/3〜5/6以上)と文中の位置(文頭・文中・文末)で行えます。選んだ結果が「学習セット」になり、学習画面は、その並びを順にたどります。

学習

  • 英文:ボタンは、「聞く」「調節」「表示」の3行です。全文は、いつも一番上に出し、その下に、「表示」で選んだ区切り(音区切り・音のつながり・意味区切り・文法区切り)を、同時に重ねられます。「表示」は、品詞・和訳・IPA・文法・単語・TOEIC解説・4択・間違いの記録も含めて、すべて独立した入り切りです(次の「英文の画面のボタン」を参照)。聞くは、全文・区切って・シャドーイング・音のつながり・聞き取り(次の節以降)。
  • 単語・意味句・IU:1枚のカードで学びます。意味(意味句は和訳)は隠してあり、見ると復習が1回増えます。IUは、声に出して練習したら「練習した」で、復習が1回増えます。意味句とIUは、文の中での位置と、文の和訳も見られます。
  • ショートカット:← → で前後、スペースで読み上げ、数字やアルファベットで表示・補助の切り替え。

英文の画面のボタン:聞く・調節・表示

英文の画面のボタンは、何をするボタンかで、3つの行に分けています。

行 ボタン
聞く ▶全文を聞く/▶区切って聞く/シャドーイング/音のつながり/聞き取り/止める
調節 速さ/間/繰り返し(間と繰り返しは、シャドーイング用)
表示 和訳/IPA/音区切り/音のつながり/意味区切り/文法区切り/品詞/文法/単語・熟語/TOEIC解説/4択問題/間違いの記録

画面の上に固定されるのは、「聞く」の行だけです。3行すべてを固定すると、英文を表示する場所が狭くなるためです(スマホで、特に効きます)。「調節」と「表示」は、ふつうにスクロールして流れます。

表示は、すべて独立した入り切り

最初は、区切りの見方を、1つだけ選ぶ形でした(選ぶと、ほかが消える)。しかし、「意味で区切った訳を見ながら、文法の区切りも見たい」ができません。そこで、「表示」の12個を、すべて独立した入り切りにしました。

  • 全文は、いつも一番上に出ます(ボタンはありません)。切り替えると全文が消える形だと、区切りと元の1文を、見比べにくいためです。
  • 区切りの見方(音区切り・音のつながり・意味区切り・文法区切り)は、同時に出せます。出した順ではなく、決まった並び(音区切り → 音のつながり → 意味区切り → 文法区切り)で、全文の下に、縦に並びます。
  • 出した区切りごとに、何を基準に区切っているかの1行の説明が付きます(例:「【意味区切り】意味のまとまりで区切り、各区切りに日本語訳を付けています…」)。見方が1つだったときは、説明も1行でしたが、同時に出すので、ブロックごとの見出しにしました。
  • 「品詞」は、区切りではなく語ごとの色なので、見方とは別の入り切りで、全文にも、出している区切りのすべてにも重なります(後述)。
  • 初期の状態は、「和訳」だけがオンです。設定は覚えていて、次に開いたときも残ります。

名前に、区切りの基準を入れた

最初は、見方の名前が「スラッシュ」「区切りの形」「IU」で、何を基準に区切っているかが、名前から分かりませんでした。そこで、基準が分かる名前に変えました。

見方 旧名 区切りの基準 付く情報
意味区切り スラッシュ 意味の切れ目 各区切りの日本語訳。前から順に意味を取る練習(スラッシュリーディング)
文法区切り 区切りの形 文法の単位 各区切りの形の名前(主語・関係詞節・前置詞句など)
音区切り IU 音の切れ目(一息で読む単位) 発音記号。シャドーイングは、この区切りで進む
音のつながり つながり くっついて聞こえる所 発音記号(つながる所は ‿)

同じ英文で比べると、基準の違いが出ます(アプリの実際のデータです)。

Ms. Tanaka, who joined the marketing team last spring, has been promoted to senior manager.

  • 意味区切り:Ms. Tanaka,(田中さんは)/who joined the marketing team last spring,(昨年の春にマーケティングチームに加わった)/has been promoted(昇進した)/to senior manager.(シニアマネージャーに)
  • 文法区切り:同じ4つに、主語/関係詞節/動詞句/前置詞句
  • 音区切り:Ms. Tanaka,/who joined … spring,/has been promoted to senior manager.(一息で読めるように、後ろの2つが1つになる)

区切りが似ていても、持っている情報が違います(訳/形の名前/発音)。画面の見た目は同じでも、「何のための区切りか」が違うので、ブロックごとの説明で、その違いを示しています。内部のデータの名前(slash・phrases・iu)と、受信箱・バックアップの形式は、変えていません。変えたのは、画面の名前と説明だけです。

const VIEWS = [   // 表示するときの並び順(ボタンの並びと同じ)
  { k: "iu",      note: "【音区切り】息継ぎの切れ目(一息で読む単位。…)で区切っています。…" },
  { k: "links",   note: "【音のつながり】くっついて聞こえやすい語のまとまりを示しています。…" },
  { k: "slash",   note: "【意味区切り】意味のまとまりで区切り、各区切りに日本語訳を付けています。…" },
  { k: "phrases", note: "【文法区切り】文法の単位(主語+動詞・前置詞句・関係詞節など)で区切り、…" }
];

「区切って聞く」は、出している区切りで読む

「▶区切って聞く」は、いま表示している区切りで、1区切りずつ読みます。複数出しているときは、「音区切り → 意味区切り → 文法区切り」の順で、最初に出ているものを使います。何も出していないときは、「文法区切り → 意味区切り → 音区切り」の順で、あるものを使い、その区切りを、自動で表示に出します(読んでいる区切りが、色付きで見えるようにするためです)。

  • シャドーイングは、音区切りがあれば音区切り、なければ文法区切り、意味区切りの順で進み、使う区切りを、自動で表示に出します。
  • 「音のつながり」を聞くは、つながりのブロックを、自動で表示に出します。
  • 読んでいる区切りは、その区切りのブロックだけで、色が付きます。

品詞は、見方ではなく入り切り

品詞の色を、「意味区切り」「文法区切り」と同じ、選ぶ列に並べると、4択のように見えて、「品詞の色を見ながら、文法の区切りも見たい」ができません。品詞は、区切りではなく、語ごとの色だからです。そこで、入り切りにして、どの表示にも(上の全文にも)重ねます。

  • 文字の色だけで表します。区切りの下線(文法の種類の色など)と、重ならないようにするためです。
  • 区切りをつなぐと、原文に戻るので、語の並びの順に、品詞を当てます(全文用と、区切りのブロックごとに、別々に数えます)。ずれていたら、その語は、色を付けません。
  • 品詞の情報が無い英文(貼り付けただけの英文など)では、色は付かず、「品詞の情報がないので、品詞の色は付きません」と出します。

ショートカットと、古い設定の引き継ぎ

  • 1 意味区切り・2 文法区切り・3 品詞・4 音区切り・5 音のつながり(それぞれ、入り切り)。0 で、区切りを全部消します。ほかの表示も、アルファベットで入り切りできます(和訳 J・IPA I・文法 G・単語 V・TOEIC T・4択 Q・記録 K)。聞くは、スペース=全文、C=区切って、R=シャドーイング、L=音のつながり、D=聞き取り、S=止める。
  • 以前の設定(見方を1つだけ選ぶ形)は、自動で移します。たとえば、「音で区切る」+「品詞の色オン」だった設定は、「音区切り」と「品詞」がオンの状態になります。

シャドーイングと聞き取り練習

英文の画面から、2つの音声練習を使えます。どちらも、端末の読み上げ機能を使うので、無料で、オフラインでも動きます。

シャドーイング

英文を、区切りごとに、「聞く → 同じ長さの無音(まねする時間)」で進めます。区切りは、**音区切り(IU)**があればIU、なければ文法区切り、意味区切り、の順で使います。IUは、息継ぎ・音の区切りなので、まねして声に出す単位に合います。

  • 画面に「聞く(2 / 3)」「声に出して、まねしてください…(2.4秒)」と出ます。読んでいる区切りは、色が付きます。
  • 間は、読み上げにかかった時間の 0.8 / 1 / 1.5 / 2倍から選びます(最短 1.2 秒)。繰り返しは、同じ区切りを 1〜3回です。
  • 最後まで終わると、復習が1回増えます。途中で止めたときは、増えません。
  • 意味句とIUのカードには、「聞いて、まねする」(その区切りだけを、聞いて、まねする時間を空ける)があります。

まねする時間は、読み上げに実際にかかった時間を測って決めます。声や速さによって、同じ文でも長さが変わるためです。測れないとき(読み上げが短すぎる、終了が通知されない)は、語数から見積もります。

function gap(spokenMs, text, mult, rate) {
  const words = text.split(/\s+/).filter(Boolean).length;
  const est = (words * 400) / (rate || 1);          // 1語 0.4秒(速さで割る)
  const base = spokenMs > 300 ? spokenMs : est;
  return Math.max(1200, Math.round(base * mult + 400));
}

「止める」や、次の英文への移動で、進行中のシャドーイングを中断できるように、読み上げに世代番号を付けています。止めたり移動したりすると番号が増え、古い読み上げの続きは、何もせずに終わります。

聞き取り練習

英文を隠して、聞いて、書いて、語ごとに答え合わせをします。「聞く」「ゆっくり聞く(0.7倍)」で、何度でも聞けます。

答え合わせは、原文と、書いた文を、語の列として比べます(大文字・小文字と、語の前後の記号は区別しません)。最長共通部分列で、合っている語を決め、残りを次の3つに分けます。

表示 意味
緑 合っていた
赤の取り消し線 聞き取れなかった(原文にあるが、書いていない)
赤+小さい字 書き間違い(原文の語と、書いた語が、並んで食い違う)
波線 余分(書いたが、原文に無い)

「聞き取れなかった」と「余分」が並んだ所は、書き間違いとして1つにまとめます。たとえば、Candidates are advised to bring copies of their certificates to the interview. を、copies→copy・their→there・the 抜けで書くと、9 / 12 語になり、どこを間違えたかが分かります。

答え合わせをすると、復習が1回増えます。同じ聞き取りで何度押しても、増えるのは1回です。

確かめたこと

テスト用のブラウザには、読み上げの声が無いので、読み上げを差し替えて確かめました。区切り3つ×繰り返し2回で、読み上げが6回呼ばれること、途中で止めると復習が増えないこと、答え合わせの色分け、復習が1回だけ増えること、は確認済みです。実際の声での「まねする時間」の長さは、実際の端末で調整する前提です(そのために、間の倍率を選べるようにしています)。

音のつながり

聞き取りが難しい理由の1つは、英語では、語と語がくっついて聞こえることです。copies of は「コピゾヴ」のように1つの音に聞こえ、綴りと耳の音が結びつきません。そこを克服するために、「つながり」という見方を足しました。

IUとの違い

最初は、IU(一息で読む単位)を、そのための単位にするつもりでした。しかし、IUは、平均5語ほどのまとまりで、「2〜3語がくっついて聞こえる」所を見るには、長すぎます。そこで、IUは、シャドーイング用にそのまま残し、IUの中の、つながって聞こえる所を、別の見方として足しました。

IU つながり
何を表すか 一息で読む単位(息継ぎ・抑揚) くっついて聞こえる語のまとまり
長さ 2〜9語(平均5語) 2〜4語
作り方 AIに位置の番号を答えさせて作る 発音辞書から、規則で作る(AIは使わない)
使い道 シャドーイング 聞き比べ・穴あきの聞き取り

つながりは、データとして保存しません。英文から、表示のたびに計算します。そのため、新しく入れた英文にも、すぐ付きます。

規則は、2つだけ

規則で確実に作れるものに絞りました。

  1. 連結:子音で終わる語に、母音で始まる語が続くとき、つなぐ。(copies of → ˈkɑpiz‿əv)
  2. 弱い語のくっつき:of・to・the・a・and・in など、弱く読まれる語が、次の語にくっつく。(to the interview. → tə ðə ˈɪntɚˌvju)
const linkable = (a, b) => !a.brk && a.ip && b.ip && !endsVowel(a.ip) && startsVowel(b.ip);  // 連結
// 次の語につなぐのは、「連結できる」か「いまの語が弱い語」のとき。1つのまとまりは、最大4語
  • 語の発音は、発音辞書から引きます。母音かどうかは、発音記号の、最初と最後の文字で決めます。
  • 句読点(, . など)をまたぐ所は、つなぎません(そこで、ふつう、間が入るためです)。
  • 辞書に無い語があるまとまりは、つなぎません(途中が抜けた発音を出さないためです)。
  • 発音記号の ‿(つながる所の印)は、連結のところに入れます。弱い語のくっつきは、空白のままで、下線で、まとまりを示します。

例(アプリの出力):

[Candidates are advised]  /ˈkændədeɪts‿ˈɑɹ‿ædˈvaɪzd/
[to bring]                /tə ˈbɹɪŋ/
[copies of their]         /ˈkɑpiz‿əv ˈðɛɹ/
certificates
[to the interview.]       /tə ðə ˈɪntɚˌvju/

samples の90文(1306語)では、2語以上のまとまりが312個できました。どの文でも、まとまりをつなぐと、原文に戻ります。

見る・聞く・書き取る

  • 表示「音のつながり」:つながって聞こえやすいまとまりに下線を引き、その下に、つながりの印つきの発音記号を出します。
  • 「音のつながり」を聞く:つながりのまとまりを、ゆっくり(0.7倍)→ふつうの速さの順に、聞き比べます。読んでいるまとまりが色付きになります。
  • 聞き取り練習の「穴あき」:つながりのまとまりだけを空欄にして、聞こえた語を書きます。答え合わせでは、正解の綴りと発音記号を並べて見せ、間違えた所は、書いた答えも出します。「聞こえた音」と「綴り」の対応を、直接鍛える形です。答え合わせで復習が1回増え、間違いがあれば、「聞き取り」の間違いとして記録します。

確かめたこと

テスト用のブラウザには、読み上げの声が無いので、読み上げを差し替えて確かめました。例の文で、まとまりが4つ(Candidates are advised、to bring、copies of their、to the interview.)に分かれること、聞き比べで、各まとまりが「0.7倍→1倍」の順に読み上げられること、穴あきの空欄が4つできて、答え合わせで、正解・不正解が分かれることは、確認済みです。実際の声で、つながりが聞こえるかは、端末で試す前提です。

できないこと

  • 脱落(t が消える)、はじき音(t が d のように聞こえる)、want to が wanna になるなどは、規則が複雑なので、表せません。
  • 辞書の発音は、強く読んだときの形です。are や it にも、強勢の印が付きます。実際の会話では、もっと弱く読まれます。
  • 読み上げは、端末の声です。声によって、つながり方が違い、実際の会話ほど、くっつかないことがあります。

単語の4択

英文の4択は、解析のときに、AIが作ります。単語の4択は、AIを使わず、単語の表から、機械的に作ります。単語は数が多く、1語ごとにAIを呼ぶと、時間も費用もかかるためです。また、誤答にする語は、手元の単語の表に、いくらでもあります。

問題の3つの形

形 問い 選択肢
英語 → 意味 「complete」/kəmˈplit/ の意味は? 意味を4つ
意味 → 英語 「〜を修了する、完了する」を英語で言うと? 単語を4つ
空所に入る語 例文の中で、その単語を ______ にしたもの(意味をヒントに出す) 単語を4つ

形は、「自動」(作れる形から、ランダムに選ぶ)か、固定を選べます。

空所の問題は、例文の中に、その単語がそのまま出てくるときだけ作ります。単語は原形で持っているので、例文で promoted と活用していると、見つけられません(samples の329語のうち、空所を作れるのは221語です)。見つかる位置は、前後が英字でない所に限ります。たとえば、complete を探すとき、completed の途中には、当てはまりません。

誤答の選び方

誤答は、ほかの単語から選びます。

  1. 単語の表から、正解の単語を除き、ランダムに並べる
  2. 正解と同じ文字列(大文字・小文字を区別しない)は除く
  3. 品詞が同じものを先に、残りを後に並べ、先頭の3つを取る
  4. 3つに足りなければ、問題を作らない

品詞をそろえるのは、「動詞の意味」に、名詞の意味が混じると、品詞だけで答えが分かってしまうためです。単語が少ないうちは、誤答が足りず、問題を作れません。画面には、「誤答にする単語が、4語以上要ります」と出します。

選択肢の並びも、ランダムです。正解が1つだけで、4つの選択肢に重なりがないことを、samples の329語で、1語ごとに5回ずつ、問題を作って確かめました(不正な問題は0件)。

答えが見えないようにする

単語のカードには、答えが見えるものが、いくつもあります。単語そのもの、意味、例文です。4択を出している間は、答えるまで、これらを隠します。

  • 「意味 → 英語」と「空所」:単語そのものが答えなので、カードの見出しを「?」にし、発音記号も隠す
  • どの形でも:意味・例文・TOEICでの使われ方を隠す

答えたあとは、すべて表示し、「正解。『complete』=〜を修了する、完了する」と出します。

記録

  • 答えると、復習が1回増えます
  • 間違えると、分類「語彙」で記録します(単語にも、英文と同じ形の間違いの記録があります)
  • 「記録」タブに、「単語の間違い N 語」と「間違えた単語を復習する」が出ます
  • 学習対象選択で、単語も「間違いあり」で絞り込めます

受信箱から届いたデータでは、単語の間違いの記録も、取り込みません(英文と同じです)。

記録と復習

学習は、やりっぱなしにすると、何が身についたかが分からなくなります。そこで、「記録」タブで、状況・復習の記録・弱点・復習の始め方を見られるようにしました。試作の弱点集計を、4つの単位に広げたものです。

状態と復習回数

英文・意味句・IU・単語のすべてに、状態(未学習・学習中・覚えた。手動で切り替え、自動では変えません)、復習回数、最終復習日があります。復習は、次のたびに1回と数えます。

単位 復習1回と数えるとき
英文 4択に答える/シャドーイングを最後まで終える/聞き取りの答え合わせをする
意味句・単語 隠した意味(和訳)を見る
IU 「練習した」を押す

数える場所は、1つの関数にまとめました。回数と最終復習日を増やすのと同時に、日ごとの記録も残します。

async function countReview(r, kind) {
  r.review_count++; r.last_reviewed = today(); await Store.put(r, kind);
  const d = Stats.ymd(), e = reviewLog[d] || (reviewLog[d] = {});
  e[KIND_UNIT[kind]] = (e[KIND_UNIT[kind]] || 0) + 1;     // 日ごとの回数(英文・単語・区切り)
}

日ごとの記録(今日・直近7日・14日の回数、連続日数、棒グラフ)は、この端末だけのものです。同期はしません。状態・復習回数・間違いは、英文などの行に付いているので、同期されます。

間違いの記録

間違いは、分類・日付・どこで間違えたかを、英文に持たせます。

  • 4択で間違えたとき:問題の論点(時制・前置詞・関係詞など)を分類にして、自動で記録します。試作は、間違えるたびに分類を選ばせていましたが、4択には、解析のときに論点が付いているので、選ぶ手間を省きました。
  • 聞き取りで間違えたとき:書き間違い・聞き取れなかった語・余分な語のどれかがあれば、分類「聞き取り」で記録します。
  • 手動:英文の「間違いの記録」で、分類を選んで追加できます。×で消せます。

分類は、4択の論点(13種)に「聞き取り」を足した14種に決めています。外から来た値は、この14種に当てはまらなければ「その他」にします。受信箱から届いたデータでは、間違いの記録は取り込みません(状態や復習回数と同じく、外から書き換えられないようにするためです)。

弱点の集計

間違いの分類ごとに、件数を数えます。期間は、直近7日・30日・全期間です。分類を押すと、その分類の英文の一覧が出て、「この分類の英文を復習する」で、そのまま学習を始められます。集計は、画面に依らない関数にして、日付を決めて試験しました。

function weak(rows, days) {   // days = 直近何日か(null なら全期間)
  const since = days ? ymd(addDays(-(days - 1))) : null;
  // 間違いごとに、分類 → 件数と英文の id を集計。多い順
}

復習の始め方

「覚えた」を除き、復習回数の少ない順に選びます。同じ回数なら、最後に復習した日が古い順で、一度も復習していないものが先です。単位と件数を選ぶほか、「間違えた英文を復習する」(選んでいる期間に間違いのある英文)もあります。

function reviewOrder(list, n) {
  // 覚えた以外 → 復習回数の少ない順 → 最終復習日の古い順(未復習が先)→ 元の並び
}

直したこと:日付が前日になっていた

最終復習日を、new Date().toISOString() の先頭10文字で作っていました。これはUTCの日付です。日本では、午前9時までは、前日の日付になります。朝に復習すると、昨日の復習になり、連続日数も狂います。端末の日付(年・月・日を、手元の時刻から作る)に直しました。

個別削除

登録した英文・単語・意味句・IUを、1件ずつ(または選んでまとめて)消せます。

  • 学習対象選択:チェックを付けて「選んだ N 件を削除」。
  • 学習中のカード:英文の画面の「この英文を削除」、単語・意味句・IUのカードの「削除」。
  • 消す前に、確認のダイアログで、何を消すかを見せます(件数、同期している端末からも消えること、元に戻せないこと)。

英文を消すと、意味句とIUも消える

意味句とIUは、英文から取り出して作った行です(親の英文の文を、ex に持っています)。親の英文だけが消えて、意味句とIUが残ると、「文の中で見る」が、元の英文を指さなくなります。そこで、英文を消すと、その英文から作った意味句・IUも、いっしょに消します。確認のダイアログには、その個数も出します(「意味句・IU(14 個)も、いっしょに削除します」)。

単語は残します。単語は、同じ単語が、ほかの英文でも使われるためです。単語を消したいときは、単語として消します。

if (u === "sentence") {
  const texts = new Set(rows.filter(r => set.has(r.id)).map(r => r.text.toLowerCase()));
  cids = chunks.filter(c => texts.has(c.ex.toLowerCase())).map(c => c.id);   // 親の英文から作った区切り
  // …英文と、その区切りを、表から消す
}

消したものは、学習セット・選択の状態からも外します。最後の1件を消すと、学習の画面は、「学習する英文が選ばれていません」に戻ります。

同期している端末からも消す

削除は、同期で、ほかの端末にも伝えます。同期は、前回の同期から変わったものだけを送ります。そのため、「前回は、手元にあった行が、いま無い」ことが、「消した」という変更になります。消した行は、中身を持たない「消した」という印(削除の記録)として送ります。

試験で、消した行が、同期で復活する不具合を見つけました(「作るなかで直したこと」に書きました)。

暗号化したクラウド同期と受信箱

設計は、試作と同じです。パスフレーズから鍵を作り(PBKDF2・60万回)、英文・単語・意味句・IUを AES-GCM で暗号化してから Cloudflare D1 に保存します。サーバーは暗号文しか持たないので、中身は読めません。2台目は、同期キー(またはQRコード)とパスフレーズで接続します。変更は、前回の同期から変わったものだけを、数秒後に送ります。

試作から変えた点が、1つあります。暗号鍵を、取り出せない形(extractable: false)で、この端末の IndexedDB に置くようにしました。試作は、鍵を文字列にして localStorage に置いていました。

受信箱

Claude Code は、利用者のデータに直接書き込めません。そこで、書き込み専用の合言葉で、内容を公開鍵(RSA-OAEP)で暗号化して預けます。復号できるのは、パスフレーズを知る端末だけです。受け取るときは、使う項目だけを取り出し、状態・復習回数は取り込みません(外から「覚えた」にはできません)。同じ英文・単語・区切りは、足りない項目だけを足します。

ローカルの Worker と D1 で、次を確かめました。保管庫の作成、受信箱の合言葉の発行、inbox-send.mjs での送信、同期での受け取り、別の端末での接続。「覚えた」にしようとした英文は未学習のまま入り、すでにある発音記号は上書きされず、手元にだけあった同じ英文は1つにまとまりました。

作るなかで直したこと

  • 並び順:貼り付けた英文が、再読み込みのたびに入れ替わりました。id が、時刻と乱数でできていて、同じ瞬間に作ったものの順が決まらなかったためです。id に連番を入れ、作った順に並ぶ形にしました。
  • 空の和訳:和訳が空文字のとき、「足りない」と見なされず、足されませんでした。空文字も、無いものとして扱うようにしました。
  • 同期で戻ってくる自分の変更:送ったものが同期で戻ってきたとき、受信として数えていました。内容が同じなら、数えないようにしました。
  • 消した行が、同期で戻っていた:同期は、送る前に、サーバーから新しい行を受け取ります。このとき、自分が前に送った行も、返ってきます。手元で消した行は、手元に無いので、「新しい行」と見なされ、復活しました。前回までに同期した行が、手元に無いときは、「この端末で消した」と判断し、戻さないようにしました。
  • 別の端末に、一部の行が届かなかった:2台目の端末で接続すると、単語 281 語(正しくは 329 語)など、一部が届きませんでした。サーバーは、1回の送信でまとめて送られた行に、同じ番号(seq)を付けます。取得は、300行ずつ、「番号が前回より大きいもの」を返しますが、ページの区切りが、同じ番号の行の途中に来ると、続きの行が、次の取得で飛ばされます。区切りの先頭の行と同じ番号の行は、そのページに入れず、次のページに回すようにしました(1回の送信は200行までで、ページより小さいので、必ず進みます)。直したあと、2台目に、全部の行が、同じ件数で届き、削除も反映されました。
  • 受信箱から受け取っても、何も出なかった:受信箱の英文が、すでに登録済みだと、重複として除かれ、何も追加されません。このとき、何も表示していなかったので、「届かない」ように見えました。受け取った通数は、必ず表示する(「新しいものは、ありませんでした」も含む)ようにし、同期の画面に、「前回の受け取り」(日時・通数・追加した数)も残すようにしました。

限界

  • 意味句・IUの発音記号は、語の発音をつないだものです。弱い発音や音のつながりは出ません。辞書に無い語(数字・固有名詞など)を含む区切りには、付きません。
  • AIの解析は、検査を通ったものだけを入れますが、内容の正しさまでは検査できません。区切りの形のラベル、文法の説明、IUの区切り位置などは、誤りが混じりえます。入れた英文を「未確認」にしているのは、そのためです。
  • 品詞の色は、文字の色だけで表します(以前は、色と下線の両方でした)。色の違いが見分けにくい人のための、「品詞名も表示」(各語の下に品詞名を出す)は、まだありません。
  • つながりは、規則で作れる連結と弱い語のくっつきだけです(脱落・はじき音などは表せません)。辞書の発音は強く読んだ形なので、実際の会話より、強勢が多く出ます。
  • 削除は、元に戻せません。同期している端末からも消えます。同じ英文を、受信箱・ファイル・サンプルで、もう一度入れれば、復活します。
  • 単語の4択の「空所」は、例文に単語がそのまま出てくるときだけ作れます(活用した形は見つけられません)。日ごとの復習の記録は、この端末だけのもので、同期されません。
  • APIの呼び出しは、返事を差し替えた試験までで、実際の Gemini の返事での確認は、これからです。