子ども向けの英文タイピング練習サイト Tanepo は、当社の有志メンバーで企画・開発・運営している小さなWebサービスです。小さなサイトですが、中身には「7言語の入力を、IMEを使わずに1つのエンジンで受ける」「5種類のキーボード配列の指づかいを1枚の表から描く」といった、業務システムの開発でもそのまま通用する設計判断がいくつも入っています。
この記事は、多言語のWebアプリや教育系サービスを企画している技術責任者・開発会社の方に向けて、その設計判断をソースコードの事実に基づいて紹介するものです。宣伝のための誇張はしません。できていないことも書きます。
この記事でわかること
Photo by Christian Wiediger on Unsplash
- IMEを使わずに、ローマ字・ハングル・ピンインを含む7言語の入力を受けるタイピングエンジンの作り方
- キーボード配列ごとの「どの指で押すか」を、描画と説明文で食い違わせない設計
- Web Speech API の読み上げを、環境が違ってもなるべく同じ声にする方法
- 7言語の翻訳漏れを TypeScript の型で止める仕組み
- 子どもの個人情報を持たないための設計と、小さく保った運用の仕組み
- AIコーディングエージェントと8週間で作った開発の進め方
結論:小さなサイトほど「後から直せない所」を先に固める
Photo by Thomas Park on Unsplash
Tanepo で効いた判断は、次の5つに集約できます。
- 入力は「キーの並び」として扱い、OSのIMEに任せない。 言語ごとの違いは「受け付けるキーの候補」の表に閉じ込め、エンジン本体は1つにする
- 同じ情報から作るものは、1つの表から作る。 キーボードの絵と指づかいの説明文を別々に書かない
- ブラウザ標準の機能で足りるものは、ブラウザに任せる。 読み上げは Web Speech API、効果音は Web Audio API で合成し、音声ファイルを1つも配信しない
- 漏れは人の注意力ではなく型とテストで止める。 翻訳の抜けは
tscが落ち、キー配列はテストが全キーを固定している - 持たなくてよいデータは持たない。 アカウントを作らせず、子どもを識別する情報を保存しない
どれも規模の大小に関係なく、後から変えると高くつく部分です。
Tanepo はどんなサービスか
Photo by Chris Ried on Unsplash
Tanepo は、6〜12歳の子どもが英語の文章を打ちながらタッチタイピングを身につけるためのサイトです。打った単語と文章全体が読み上げられ、次に押すキーと使う指が画面のキーボード上に示されます。
| 項目 | 内容 |
|---|---|
| 対象 | 6〜12歳。物理キーボードのあるPCでの利用を前提にしている |
| 画面の言語 | 7言語(英語・日本語・簡体中文・韓国語・スペイン語・フランス語・イタリア語) |
| 打つ言語 | 英語に加えて、ローマ字(日本語)・ピンイン(中国語)・ハングル(韓国語)・スペイン語・フランス語・イタリア語 |
| キーボード配列 | US・JIS・AZERTY(フランス語)・スペイン語・イタリア語の5種類 |
| コンテンツ | レッスン、短い絵本、単語練習、タイムアタック、結果の共有カード |
| 技術スタック | Next.js 16(App Router)・React 19・TypeScript・PostgreSQL 18・Web Speech API |
| 開発期間 | 2026年7月26日〜9月20日(約8週間・260コミット) |
本番の実行時に読み込むライブラリは、Next.js と React のほかに PostgreSQL のドライバ・入力検証の zod・QRコード生成など数個だけです。日本語の読みやピンインを付ける形態素解析ライブラリは、ビルド前のデータ生成でだけ使い、ブラウザには送っていません。
設計1:IMEを使わずに7言語を打たせる入力エンジン
なぜIMEをオフにするのか
日本語・中国語・韓国語の入力は、ふつうは OS の IME(変換ソフト)が担います。しかしタイピング練習でIMEを通すと、変換候補の確定や未確定文字の扱いがブラウザとOSの組み合わせごとに違い、「何のキーを押したか」を正確に判定できません。
そこで Tanepo では、すべての言語をIMEオフのまま、ラテン文字のキー入力の並びとして判定しています。キーボードのイベントを受ける処理は、IMEの変換中(isComposing)と、Ctrl・Command・Alt との同時押しを最初に捨てます。
エンジンは1つ、言語の違いは「候補の表」に閉じ込める
判定の本体は、200行に満たない純粋な状態機械です。打つべき文章を「入力単位」の列に分解し、各単位に受け付けるキーの並びの候補を持たせます。正しいキーなら次へ進み、違うキーならカーソルは進めずにミスとして数えます。キーごとのミスの回数も記録します。
言語ごとの違いは、この「候補」の作り方だけです。
日本語(ローマ字) は、Google日本語入力と Microsoft IME の綴りに合わせた表を使っています。ここで覚えた打ち方が、本物のIMEでもそのまま通じるようにするためです。
// し は shi / si / ci、じ は ji / zi(zhi はピンインなので受け付けない)
し: ["shi", "si", "ci"], ち: ["chi", "ti"], つ: ["tsu", "tu"], ふ: ["fu", "hu"],
「ん」と「っ」は、次に来る文字によって受け付ける綴りが変わるので、表ではなく分解処理の側で扱います。たとえば「っ」は次の子音を重ねて打つのが基本で、次が母音や句読点のときは単独の xtu / ltu になります。
韓国語(ハングル) は、Unicode の音節が「初声19 × 中声21 × 終声28」の組み合わせで並んでいる性質を使い、1文字を計算で字母に分解して、2ボル式キーボードのキーの並びに対応させています。IMEがオンなら同じキーの並びで同じ文字が組み上がるので、ここでの練習がそのまま実際の入力になります。
中国語(ピンイン) は、漢字を表示し、声調なしのピンインを打たせます。「ü」は一般的なピンイン入力と同じく v で打ちます。漢字の読みはビルド前に生成したデータを使います。
アクセント記号のための例外処理
スペイン語・フランス語・イタリア語の「é」のような文字は、デッドキー(アクセント記号のキーを押してから文字を押す方式)で入力します。Firefox や一部の macOS の配列では、この入力がキーイベントではなく「1文字の変換確定」として届きます。Tanepo では、確定した文字がちょうど1文字のときだけキー入力として扱い、2文字以上の確定(=IMEの変換)はこれまでどおり無視しています。
指標の定義
速度は、5打鍵を1単語として数える一般的な WPM(1分あたりの単語数)で出しています。1秒未満の入力では数字に意味がないので 0 を返します。正確さは、印字されるキーの打鍵だけを分母にし、0% を下回らないようにしています。
設計2:5種類のキーボード配列を、1枚の表から描画と説明に使う
指づかいのガイドは、画面に描くキーボードと、「左手の小指で押します」のような説明文の2つで構成されています。この2つを別々に書くと、いつか必ず食い違います。
Tanepo では、キーごとの位置・文字・担当する指を1つの表に持ち、キーボードの描画と説明文の両方をそこから生成しています。表はテストで全キーが固定されているので、誤って書き換えるとテストが落ちます。
| 配列 | 何が変わるか |
|---|---|
| US | 基準 |
| JIS | 記号の位置と、小指で押す記号の説明 |
| AZERTY | 文字の位置そのもの(A と Q、Z と W など) |
| スペイン語・イタリア語 | アクセント付きの文字とデッドキー |
どの指でどのキーを押すかは、各指がホームポジションの上下の斜めの列を受け持つ方式に統一しています。数字の段を1つずらして割り当てる古くからの流儀もありますが、「R を押す指と 4 を押す指を同じにする」ために採用しなかった理由も設計メモに残しています。
ガイド自体は文字を使わず、光る指とキーで伝えます。読み上げ用の説明文はスクリーンリーダー向けに別途流しています。
設計3:読み上げは Web Speech API、効果音は Web Audio API で合成する
音声ファイルを1つも配信しない
読み上げは、ブラウザ標準の Web Speech API の音声合成だけで行っています。事前に録音・生成した音声ファイルはありません。キーを押したときの効果音も、Web Audio API の発振器でその場で合成しています。
効果音の AudioContext は最初のキー入力のときに作ります。ブラウザは「ユーザー操作の前に音を出す」ことを制限しているため、ページを開いた時点で作るとこの制限に引っかかります。
環境が違っても、なるべく同じ声にする
Web Speech API の弱点は、使える声がOSとブラウザで違うことです。何も指定しないと、環境ごとに別の声で読まれます。
Tanepo では、英語の声を優先順位つきの固定リストから決めています。Chrome はどのOSでも「Google US English」を持っているので、これを最優先にすると多くの環境で同じ声になります。無ければ macOS の Samantha、Windows の Microsoft Aria などに順に落とします。
もう1つ大事にしているのは、読み上げが失敗しても入力を止めないことです。再生を直列に並べる制御クラスを1つ挟み、失敗は握りつぶします。音が出なくても練習は続けられます。読み上げる言語は画面の言語とは別に選べます(英文を打ちながら日本語の訳を聞く、といった使い方ができます)。
設計4:7言語の翻訳漏れを、型で止める
このサイトのルールは「7言語すべてで動かないものは完成ではない」です。英語で先に作って後から翻訳する、という進め方はしません。
画面に出る文字列はすべて、言語をキーにした Record<UiLocale, …> 型のテーブルに置いています。どれか1言語を書き忘れると tsc(型チェック)が通らないので、翻訳漏れのまま本番に出ることが構造的に起きません。
URLは英語が素のパス、他の言語は /ja/... のような接頭辞つきのパスにしています。このURL設計とクロールの実測は、別記事の多言語サイトのURLを ?hl= からパスに変えた実測記録にまとめています。ここでは、その後に直した不具合を1つ紹介します。
接頭辞つきのURLは、Next.js の proxy(旧 middleware)で接頭辞を外して書き換え、どの言語で描くかをリクエストに載せて渡しています。当初はこれを cookie で渡していたため、古い版で日本語を選んだことのあるブラウザでは、トップページ / が日本語で表示されるという不具合がありました。cookie はブラウザに残り続けるからです。現在はリクエストヘッダーで渡しています。ブラウザが自分から送ることはないので、訪問者もクローラーも間違った言語に迷い込みません。
コードのコメントには、「本来の解決は言語をルートの階層として持つことで、それまではこの書き換えを"整理"しないこと」と、残っている負債も明記しています。
インデックスの制御も同じ考え方です。検索に出してよいのはサイトマップに載せたページだけで、それ以外のページには一律に noindex を返します。新しく足したページは、誰かが明示的に載せるまで検索に出ません。
設計5:子どもを識別するデータを持たない
子ども向けサービスなので、データは「持たない」ことを基本にしています。
- アカウント登録はありません。設定はブラウザ内にだけ保存します
- 練習結果を受け取るAPIは入力を検証しますが、保存の設定が無効なら何も保存せずに応答します。保存する場合も匿名の数値(時間・打鍵数・ミス数など)だけで、IPアドレスや User-Agent は保存せず、ログにも本文を出しません
- タイムアタックの履歴は各ブラウザの中だけに置き、サーバには送りません
データベース側にも、練習時間・打鍵数・正確さ・WPM が物理的にありえない値にならないよう制約をかけています。アプリの検証をすり抜けても、DBが受け付けません。
設計6:運用を小さく保つ
少人数で回すサイトなので、運用の仕組みも小さくしています。
- CI は置かず、デプロイスクリプト1本に検査を集約。 lint・型チェック・テストを手元で通してから転送し、サーバでビルドします。本番に触れるのは最後の再起動だけなので、ビルドやマイグレーションが失敗しても旧版が動き続けます
- 見えない壊れ方を毎回確かめる。 練習結果を共有したときのプレビュー画像(OGP画像)は、壊れてもアプリは動き、共有したリンクの画像が空白になるだけです。気づきにくいので、デプロイのたびに画像を取得し、本物のPNGか(ファイル先頭のシグネチャと10KB以上のサイズ)を確認しています
- テスト。 ユニットテスト・統合テストと、ブラウザで実際に操作するE2Eテスト(練習・言語切り替え・共有カード・絵本など)を合わせて、約370件あります
また、数字を動かすための変更には、公開前にベースラインを測り、判定日を決めた計測メモを同じコミットで残すルールにしています。効かなかった施策も消さずに残しています。途中で、クローラーの訪問を人のアクセスとして数えていて、訪問者数を約10倍に見積もっていたことにも気づき、計測方法ごと直しました。
システム開発・AI導入について相談する
業務システム開発・生成AI導入・Dandori AIに関するご相談は、シンシアへお気軽にどうぞ。無料で相談する →まだ検討段階の方は 質問だけでもOK
AIコーディングエージェントと8週間で作った
Tanepo は、AIコーディングエージェント(Claude Code)と一緒に作りました。260コミットのうち187コミット(約72%)に、AIとの共同作成を示す記録が残っています。
ただし、AIに丸ごと任せたわけではありません。効いたのは、人が決めることと、AIに守らせることを文書とテストで分けたことです。
| 人が決めたこと | 仕組みで守らせたこと |
|---|---|
| 7言語で完全に動かすという方針 | 翻訳漏れは tsc が落ちる |
| 指づかいの流儀(列ごとに担当する方式) | 全キーをテストで固定 |
| 子どものデータを持たない | 保存する項目をAPIの型と設定で限定 |
| 数字を動かす変更は計測してから | 計測メモのテンプレートとルールを CLAUDE.md に明記 |
AIは指示の行間を読んで「それらしく」直そうとします。たとえば先の proxy の処理は、一見無駄に見えるため整理したくなる箇所ですが、消すと全言語のページが英語になります。だからこそ、そこには理由と「消すな」をコメントで残しました。 AIが書くコードの量が増えるほど、人が書くべきなのはコードよりも、判断の理由と守るべき線です。この考え方は、Claude CodeでAIエージェントへの指示方法をAnthropicが解説した記事でも触れています。
受託開発に持ち帰れること
Tanepo は小さな教育サービスですが、ここで使った判断は業務システムにもそのまま当てはまります。
- 多言語の業務画面では、翻訳漏れを人のレビューで見つけるのは限界があります。型で止める仕組みを最初に入れておくと、言語を足すたびの品質が安定します
- キーボード入力が業務の中心になる画面(受注入力・検品・コールセンターなど)では、IMEの状態やブラウザの差で入力が取りこぼされる問題が実際に起きます。入力を「キーの並び」として扱う設計は、こうした画面の不具合対応にも使えます
- 個人情報を持たない設計は、セキュリティ対策の費用そのものを減らします。保存しないデータは、漏れることもありません。どこまで持たずに済むかは、非機能要件を決める段階で議論しておくのが安上がりです
- AIエージェントを使った開発では、生産性はAIの能力より「何を人が決め、何を仕組みで守らせるか」の設計で決まります。AIシステム開発の進め方でも同じ考え方を解説しています
よくある誤解
「IMEをオフにして練習しても、日本語入力の練習にならない」 ローマ字の綴りの表を Google日本語入力と Microsoft IME に合わせてあるので、ここで打つキーの並びは、IMEをオンにしたときと同じです。違うのは変換操作だけです。
「読み上げは録音した音声の方が品質が高い」 声の自然さは録音に分があります。ただし7言語分の文章をすべて録音・管理するのは、少人数の運営では続けられません。Tanepo は、声の揺れを優先順位つきの選択で抑えたうえで、ブラウザの音声合成を選んでいます。
「CIが無いのは品質管理が甘い」 検査を省いているわけではなく、検査をデプロイスクリプトに集約しています。検査を通らないものはサーバに送られません。
FAQ
Q. Web Speech API の声は、ユーザーの環境によって変わりますか?
変わります。使える声はOSとブラウザが持っているものだけです。Tanepo では、Chrome がどのOSでも持っている「Google US English」を最優先にした固定の優先順位で声を選び、できるだけ同じ声になるようにしています。
Q. スマートフォンでも使えますか?
練習は物理キーボードのあるPCを前提にしています。画面キーボードではタッチタイピングの練習にならないためです。読み物のページはスマートフォンでも読めるようにしています。
Q. 練習の記録はどこに保存されますか?
タイムアタックの履歴は各ブラウザの中だけに保存され、サーバには送りません。サーバ側で保存する場合も、時間や打鍵数などの匿名の数値だけで、IPアドレスや User-Agent は保存しません。
Q. 同じような多言語のWebアプリを依頼できますか?
はい。当社では、多言語対応・入力まわりの設計・AIエージェントを使った開発体制づくりを含めてご相談をお受けしています。
まとめ
- Tanepo は、7言語の入力をIMEオフのまま1つの状態機械で判定し、言語の違いを「受け付けるキーの候補」の表に閉じ込めている
- キーボードの描画と指づかいの説明は、テストで固定した1枚の表から作っている
- 読み上げと効果音はブラウザ標準の機能だけで実現し、音声ファイルを配信していない
- 翻訳漏れは型で、配列の誤りはテストで、見えない壊れ方はデプロイ時の確認で止めている
- AIエージェントと作るときほど、人は「判断の理由」と「守る線」を書き残す必要がある
次に読むなら、URL設計とクロールの実測をまとめた多言語サイトのURLを ?hl= からパスに変えた実測記録と、自社プロダクトの開発の考え方を書いたDandori AI開発ストーリーがおすすめです。