Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ This project uses static HTML, CSS, and classic JavaScript. No build, npm instal
- `padded/`: With margins for individual character examination
- `full/`: Generated root viewBox expansion, used by both text displays and PNG export
5. **dancingmen-logic.js**: DOM-independent functions, exposed through globalThis.DancingMenLogic and CommonJS
6. **dancingmen-messages.js**: Japanese dictionary and formatter, exposed through globalThis and CommonJS
7. **test/**: Logic, samples, SVG integrity, layout, messages, HTML, contrast, formatting, static code, and README tests
8. **tools/build-full-svg.js**: Dependency-free Node generator, not used by the browser
6. **dancingmen-messages.js**: Japanese and English dictionaries sharing one set of keys, plus the formatter,
exposed through globalThis and CommonJS. It must stay free of DOM, storage and network references
7. **i18n.js**: Language selection (query string, localStorage, browser setting), `data-i18n` application and the
`languagechange` event. It is the only file that touches localStorage
8. **test/**: Logic, samples, SVG integrity, layout, messages, i18n, HTML, contrast, formatting, static code, and README tests
9. **tools/build-full-svg.js**: Dependency-free Node generator, not used by the browser

### Key Architectural Decisions

Expand All @@ -49,7 +52,10 @@ This project uses static HTML, CSS, and classic JavaScript. No build, npm instal

4. **Event-Driven UI**: No inline handlers or styles. Decryption uses a token-line array rather than reading state from the DOM.
Tabs use roving tabindex and ArrowLeft/ArrowRight/Home/End without hard-coding the number of tabs.
The substitution table is built once. Only its buttons open the native dialog; Escape and close restore focus.
The substitution table, the decryption buttons and the sample list are rebuilt whenever the language changes;
only the table's buttons open the native dialog, and Escape or close restores focus.
Status messages are held as `{ key, values }` in `statusMessages` and translated when drawn, so switching the
language re-renders them instead of wiping them.

### Cipher Implementation

Expand Down Expand Up @@ -81,9 +87,16 @@ but PNG export deliberately shows the local-server guidance instead. Chromium on
Clipboard absence or rejection is handled as a status message, without execCommand fallback.
Keep the meta CSP, no-referrer policy, and noopener noreferrer links. Meta CSP does not provide frame-ancestors protection.

### Japanese-First Design
### Bilingual UI (Japanese and English)

The UI is Japanese. All dynamically displayed text, alt/title/aria labels, and messages must go through `t(key, values)`
and dancingmen-messages.js. The formatter rejects unknown keys and missing values. Do not add Japanese string literals to
script.js or dancingmen-logic.js. English dictionaries and a language switch are deferred to a later iteration.
Static HTML text may remain Japanese. Keep the README's sample/rule tables, file tree, and screenshots consistent with the code.
Japanese is the default and English is a peer, not an afterthought. Every phrase lives in dancingmen-messages.js under a
key that both dictionaries define, and every dynamically displayed text, alt/title/aria label and status message goes
through `t(key, values)`. The formatter rejects unknown keys and missing values.

- Static markup carries `data-i18n`, `data-i18n-aria-label`, `data-i18n-placeholder`, `data-i18n-title` or `data-i18n-alt`.
`apply()` replaces `textContent`, so an element with child elements never takes `data-i18n`; wrap its parts in spans
- Never branch on displayed text. Use `dataset` marks or the shape of the value instead
- Do not add Japanese string literals to script.js or dancingmen-logic.js; the dictionary is the only home for wording
- The `noscript` message is bilingual, because no script can translate it
- `test/i18n.test.js` locks the key sets, the placeholder names, the load order and the status-message pattern
- Keep both READMEs, the sample/rule tables, the file tree and the screenshots consistent with the code
365 changes: 365 additions & 0 deletions README.en.md

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ hub: true
---
-->

[English](README.en.md) · 日本語

# DancingMen CipherLab - ドイルの踊る人形暗号ツール

![GitHub Repo stars](https://img.shields.io/github/stars/ipusiron/dancingmen-cipherlab?style=social)
Expand Down Expand Up @@ -99,7 +101,7 @@ hub: true
### その他の機能

- レスポンシブデザイン対応
- 日本語UI(シャーロキアン向け)
- 日本語UIと英語UIの切り替え(右上のボタン。選択はブラウザーに記憶され、`?lang=ja`・`?lang=en`でも指定可能)
- 純粋なクライアントサイド実装(外部API不要)
- タブ・復号ボタン・置換表の拡大をキーボードだけで操作可能

Expand Down Expand Up @@ -187,7 +189,7 @@ hub: true
- **SVGの3バージョン管理**: full版(表示・PNG保存用)、padded版(参照・選択用)、tight版(full版の生成元)
- **モーダルシステム**: ネイティブのdialog要素による置換表の画像拡大
- **イベント駆動設計**: 効率的なイベントデリゲーションによるUI制御
- **分離**: 純粋ロジックはdancingmen-logic.js、文言の辞書はdancingmen-messages.js、画面処理はscript.js
- **分離**: 純粋ロジックはdancingmen-logic.js、文言の辞書はdancingmen-messages.js、言語の選択はi18n.js、画面処理はscript.js

---
## 🖋 使用フォントについて
Expand Down Expand Up @@ -288,8 +290,14 @@ full版は`node tools/build-full-svg.js`で作り直せます。

置換表の拡大にはネイティブの`dialog.showModal()`を使い、Esc・閉じるボタン・背景クリックで閉じます。
閉じた後は、開いたボタンへフォーカスを戻します。
JavaScriptが表示する文言は`dancingmen-messages.js`の日本語辞書から`t`で取得します。
英語辞書と言語切り替えは未実装です。
JavaScriptが表示する文言は`dancingmen-messages.js`の辞書から`t`で取得します。
辞書は日本語と英語が同じキーを共有し、`i18n.js`が選択中の言語を決めて`data-i18n`の付いた要素へ当てます。

### 言語の切り替え

`?lang=ja`・`?lang=en`、`localStorage`に保存した選択、ブラウザーの言語設定の順に見て、初期表示の言語を決めます。
右上のボタンで切り替えると`languagechange`イベントが起き、人形の代替テキスト・置換表・サンプル一覧・状態メッセージを作り直します。
入力欄の内容と復号中の人形は残ります。

---
## 🔒 セキュリティ
Expand All @@ -299,6 +307,7 @@ JavaScriptが表示する文言は`dancingmen-messages.js`の日本語辞書か
- 入力の正規化と長さ制限を実施。入力・貼り付け・メッセージはtextContentで表示
- 外部API・CDN・アクセス解析を使用せず、ページの表示と操作で外部への通信は0件
- コピーAPIが使えない、または拒否された場合は手動コピーの案内を表示
- 入力した文はブラウザーの外に出ません。`localStorage`に保存するのは言語の選択だけです

外部リンクをユーザーが開いたときは、そのリンク先へ通信します。
metaのCSPではクリックジャッキングを防げません。
Expand All @@ -316,7 +325,7 @@ node tools/build-full-svg.js --check
```

GitHub Actionsはpushとpull_requestの両方で`npm test`を自動実行します。
旗の規則・正規化・表記の往復・寸法・PNG配置・配色・HTML・辞書・READMEの表を検証します。
旗の規則・正規化・表記の往復・寸法・PNG配置・配色・HTML・辞書・言語の切り替え・READMEの表を検証します。
既存SVG素材104枚の無改変をSHA-256で検証し、full版52枚が生成スクリプトの出力と一致することも検証します。

---
Expand Down Expand Up @@ -433,9 +442,11 @@ dancingmen-cipherlab/ # 「踊る人形」の暗号を体験す
├── CLAUDE.md # AI向けの開発ガイド
├── dancingmen-logic.js # 画面に依存しない純粋なロジック
├── dancingmen-messages.js # 画面の文言の辞書とフォーマッター
├── i18n.js # 日本語・英語の切り替え(辞書はmessages側)
├── index.html # 暗号化・復号・置換表の3タブ
├── LICENSE # 本ツールのMITライセンス
├── package.json # 依存なしのnpm test定義
├── README.en.md # 英語版のドキュメント
├── README.md # 本ドキュメント
├── script.js # 入力・タブ・描画・PNG保存・モーダル
├── style.css # CSS変数の配色とレスポンシブレイアウト
Expand All @@ -444,6 +455,7 @@ dancingmen-cipherlab/ # 「踊る人形」の暗号を体験す
│ ├── contrast.test.js # 文字色と面のコントラストを検証
│ ├── format.test.js # 行長と読みやすさを検証
│ ├── html.test.js # CSP・ARIA・インライン属性なしを検証
│ ├── i18n.test.js # 辞書の対応・data-i18n・切り替えを検証
│ ├── layout.test.js # 人形の寸法・viewBoxの拡張・PNG配置を検証
│ ├── logic.test.js # 正規化・旗の規則・表記変換・往復を検証
│ ├── messages.test.js # 文言の辞書と画面側のキーを検証
Expand Down
123 changes: 122 additions & 1 deletion dancingmen-messages.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
// UI-independent messages. Additional languages can share the same keys.
const DancingMenMessages = (() => {
const DEFAULT_LANG = "ja";
const LANGUAGES = ["ja", "en"];
const dictionaries = {
ja: {
"list.open": "「",
"list.close": "」",
"app.title": "DancingMen CipherLab - ドイルの踊る人形暗号ツール",
"app.description": "シャーロック・ホームズの「踊る人形」暗号を体験できるツールです。",
"app.langButton": "English",
"app.langAria": "言語を切り替える",
"tabs.aria": "暗号化・復号・置換表",
"tab.encrypt": "🔐 暗号化",
"tab.decrypt": "🔓 復号",
"tab.table": "📓 置換表",
"encrypt.inputLabel": "平文を入力してください(A〜Zとスペース、改行)",
"encrypt.placeholder": "HELLO WORLD のように入力",
"encrypt.hint": "A〜Zとスペース、改行が使えます。全角の英字とスペースは半角に置き換えます。",
"encrypt.button": "暗号化",
"sample.legend": "原作の暗号文",
"sample.load": "入力欄に入れる",
"save.button": "暗号文をPNGで保存",
"output.filenames": "暗号文(ファイル名の列)",
"output.font": "暗号文(GL-DancingMenフォント用の文字列)",
"output.fontNote": "小文字が旗なし、大文字が旗あり。GL-DancingMenフォントを指定すると、そのまま踊る人形になります。" +
"復号タブへ貼り付けると元の文に戻せます。",
"copy.fontButton": "フォント用の文字列をコピー",
"decrypt.intro": "下の人形をクリックするか、Tabで選んでEnterを押すと、対応する文字を復元できます。",
"decrypt.pick": "▼ 以下の人形を選んで復号対象に追加します。",
"decrypt.plainHeading": "旗なし",
"decrypt.flagHeading": "旗あり(語の終わり)",
"decrypt.pasteLabel": "暗号文のテキストを貼り付ける(フォント用の文字列、またはファイル名の列)",
"decrypt.pasteButton": "貼り付けた暗号文を復号",
"decrypt.figuresHeading": "入力した人形",
"decrypt.clear": "クリア",
"decrypt.delete": "1文字削除",
"decrypt.resultHeading": "復号結果",
"copy.decryptButton": "復号結果をコピー",
"table.intro": "以下はアルファベットと踊る人形の対応表です(平文文字がアルファベット、暗号化文字が人形文字)。",
"table.zoomHint": "クリック、またはTabで選んでEnterを押すと拡大表示されます。",
"table.flagHeading": "🚩 旗ありの意味",
"table.flagLead": "「旗あり」の人形は、その直後に",
"table.flagStrong": "空白が来る",
"table.flagRest": "ことを示します。続く行に英字がある場合も、行末の文字に旗が付きます。空行は通の区切りです。",
"table.flagExample": "たとえば \"HELLO WORLD\" では、\"O\" の次がスペースなので \"Of.svg\"(旗付き)が使われます。",
"table.sampleLabel": "サンプル:",
"table.sampleAlt": "Of(旗あり)",
"table.sampleNote": " は \"O\" + 空白",
"modal.aria": "人形の拡大表示",
"modal.close": "閉じる",
"footer.repo": "🔗 GitHubリポジトリはこちら(",
"footer.repoEnd": ")",
"feedback.removed": "使えない文字を取り除きました: {chars}",
"feedback.replaced": "全角の英字・スペースやタブを半角に置き換えました",
"feedback.truncated": "{max}文字を超えた分は切り捨てました",
Expand All @@ -28,6 +74,81 @@ const DancingMenMessages = (() => {
"decode.invalid": "読めなかった部分: {chars}",
"decode.more": " ほか{count}件",
"copy.decryptSuccess": "復号結果をコピーしました"
},
en: {
"list.open": "“",
"list.close": "”",
"app.title": "DancingMen CipherLab - Sherlock Holmes Dancing Men Cipher Tool",
"app.description": "A tool for trying out the Dancing Men cipher from Arthur Conan Doyle's Sherlock Holmes.",
"app.langButton": "日本語",
"app.langAria": "Switch language",
"tabs.aria": "Encrypt, decrypt and substitution table",
"tab.encrypt": "🔐 Encrypt",
"tab.decrypt": "🔓 Decrypt",
"tab.table": "📓 Substitution table",
"encrypt.inputLabel": "Enter the plaintext (A-Z, spaces and line breaks)",
"encrypt.placeholder": "Type something like HELLO WORLD",
"encrypt.hint": "A-Z, spaces and line breaks are allowed. Full-width letters and spaces become half-width.",
"encrypt.button": "Encrypt",
"sample.legend": "Messages from the original story",
"sample.load": "Put it in the input box",
"save.button": "Save the ciphertext as a PNG",
"output.filenames": "Ciphertext (a list of file names)",
"output.font": "Ciphertext (a string for the GL-DancingMen font)",
"output.fontNote": "Lower case means no flag and upper case means a flag. Apply the GL-DancingMen font " +
"and the string turns into dancing men. Paste it into the decrypt tab to get the original text back.",
"copy.fontButton": "Copy the font string",
"decrypt.intro": "Click a figure below, or select it with Tab and press Enter, to restore the letter it stands for.",
"decrypt.pick": "▼ Choose figures below to add them to the decryption.",
"decrypt.plainHeading": "Without a flag",
"decrypt.flagHeading": "With a flag (end of a word)",
"decrypt.pasteLabel": "Paste the ciphertext (a font string, or a list of file names)",
"decrypt.pasteButton": "Decrypt the pasted ciphertext",
"decrypt.figuresHeading": "Figures entered",
"decrypt.clear": "Clear",
"decrypt.delete": "Delete one letter",
"decrypt.resultHeading": "Decrypted text",
"copy.decryptButton": "Copy the decrypted text",
"table.intro": "The table below pairs every letter of the alphabet with its dancing man " +
"(the plaintext letter first, the cipher figure after it).",
"table.zoomHint": "Click a figure, or select it with Tab and press Enter, to see it enlarged.",
"table.flagHeading": "🚩 What the flag means",
"table.flagLead": "A figure that holds a flag tells you that ",
"table.flagStrong": "a space follows",
"table.flagRest": " right after it. The last letter of a line also carries a flag when the next line " +
"still has letters. A blank line separates one message from the next.",
"table.flagExample": "In \"HELLO WORLD\", for instance, a space follows the \"O\", " +
"so \"Of.svg\" (with a flag) is used.",
"table.sampleLabel": "Example: ",
"table.sampleAlt": "Of (with a flag)",
"table.sampleNote": " stands for \"O\" followed by a space",
"modal.aria": "Enlarged view of a figure",
"modal.close": "Close",
"footer.repo": "🔗 GitHub repository: ",
"footer.repoEnd": "",
"feedback.removed": "Removed the characters that cannot be used: {chars}",
"feedback.replaced": "Full-width letters, full-width spaces and tabs became half-width",
"feedback.truncated": "Everything past {max} characters was cut off",
"feedback.separator": " / ",
"encrypt.count": "Characters that can be encrypted: {count}",
"figure.letter": "{letter}",
"figure.flag": "{letter} (with a flag)",
"sample.label": "Message {number}: {text}",
"sample.lineSeparator": " / ",
"sample.multiline": " (2 lines)",
"sample.all": "All {count} messages (separated by blank lines)",
"copy.fontSuccess": "Copied the font string",
"copy.failure": "Could not copy. Select the text and copy it by hand",
"save.tooLarge": "The image is too large to save. Shorten a line, or use fewer lines",
"save.unavailable": "A PNG cannot be saved while this page is open over file://. " +
"Open the published page, or serve the folder locally (python -m http.server, for example)",
"save.success": "Saved the ciphertext as a PNG",
"figure.plain": "{letter} (no flag)",
"figure.zoom": "Enlarge {figure}",
"decode.tooLong": "Ciphertext with more than {max} figures cannot be decrypted. Shorten it before pasting",
"decode.invalid": "Parts that could not be read: {chars}",
"decode.more": " and {count} more",
"copy.decryptSuccess": "Copied the decrypted text"
}
};

Expand All @@ -45,7 +166,7 @@ const DancingMenMessages = (() => {
});
}

return { DEFAULT_LANG, dictionaries, format };
return { DEFAULT_LANG, LANGUAGES, dictionaries, format };
})();

globalThis.DancingMenMessages = DancingMenMessages;
Expand Down
Loading
Loading