From e206b860efe0e23f0757e3ebaf38e2239313dbca Mon Sep 17 00:00:00 2001 From: ipusiron Date: Mon, 28 Sep 2026 00:23:00 +0900 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=A8=E8=8B=B1?= =?UTF-8?q?=E8=AA=9E=E3=81=AE=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=81=AB?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 既存のdancingmen-messages.jsに英語辞書を足し、DOM側の言語選択を i18n.jsへ切り出した。HTMLの文言も同じ辞書へ移し、data-i18nで当てる。 - 辞書は68キー×2言語。キー・差し込み名の一致をテストで縛る - ?lang → localStorage → ブラウザー設定の順で初期言語を決める - 切り替え時は人形の代替テキスト・置換表・復号ボタン・サンプル一覧を作り直す - テストは60件から72件へ 見つけた不具合を2件直した。 1. 状態メッセージが言語切り替えで消えていた。翻訳済みの文字列を要素へ 直接入れていたため、作り直しの過程で空になっていた。{key, values}で 覚えておき、描く直前に訳す形へ変更した 2. 320px幅の英語表示でタブが横にはみ出していた(scrollWidth 355)。 .tab-buttonのwhite-space: nowrapが原因。折り返しを許してmin-width: 0を 足し、320pxに収めた Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 31 +++- README.en.md | 365 +++++++++++++++++++++++++++++++++++++++++ README.md | 22 ++- dancingmen-messages.js | 123 +++++++++++++- i18n.js | 78 +++++++++ index.html | 99 ++++++----- script.js | 105 ++++++++---- style.css | 24 ++- test/html.test.js | 5 +- test/i18n.test.js | 139 ++++++++++++++++ 10 files changed, 900 insertions(+), 91 deletions(-) create mode 100644 README.en.md create mode 100644 i18n.js create mode 100644 test/i18n.test.js diff --git a/CLAUDE.md b/CLAUDE.md index 760b3a5..5142231 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 @@ -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 @@ -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 diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..1fb391e --- /dev/null +++ b/README.en.md @@ -0,0 +1,365 @@ +English · [日本語](README.md) + +# DancingMen CipherLab - a Dancing Men cipher tool + +![GitHub Repo stars](https://img.shields.io/github/stars/ipusiron/dancingmen-cipherlab?style=social) +![GitHub forks](https://img.shields.io/github/forks/ipusiron/dancingmen-cipherlab?style=social) +![GitHub last commit](https://img.shields.io/github/last-commit/ipusiron/dancingmen-cipherlab) +![GitHub license](https://img.shields.io/github/license/ipusiron/dancingmen-cipherlab) +[![GitHub Pages](https://img.shields.io/badge/demo-GitHub%20Pages-blue?logo=github)](https://ipusiron.github.io/dancingmen-cipherlab/) + +**Day022 - Security Tools 100 with Generative AI** + +**DancingMen CipherLab** reproduces the cipher from *"The Adventure of the Dancing Men"*, the Sherlock Holmes +story by Arthur Conan Doyle. +Type a message and watch it turn into stick figures, rebuild a message by picking figures one at a time, and +look up the whole substitution table. +The six ciphertexts from the original story are included as samples. + +--- + +## 🌐 Demo + +👉 [https://ipusiron.github.io/dancingmen-cipherlab/](https://ipusiron.github.io/dancingmen-cipherlab/) + +The page opens in Japanese or English depending on the browser setting, and the button in the top-right corner +switches between them at any time. `?lang=en` and `?lang=ja` force a language. + +--- + +## 📸 Screenshots + +![Encrypting a message](assets/screenshot.png) + +> *The fifth message of the story, entered on two lines. The O that ends the first line also carries a flag.* + +![Decrypting a message](assets/screenshot2.png) + +> *The font string "comE herE aT oncE" pasted in and turned back into "COME HERE AT ONCE".* + +![Substitution table](assets/screenshot3.png) + +> *Every letter from A to Z, without a flag and with one. Each figure enlarges by mouse or by keyboard.* + +--- + +## ✨ Features + +### Encrypt tab + +- Letters A-Z become the matching dancing man as you type +- A space is carried by the preceding figure as a flag (`A` + space = `Af.svg`) +- Full-width letters, full-width spaces and tabs are folded to half-width, and IME input is handled after it is committed +- Seven samples: the six messages of the original story, plus all of them at once +- Two notations are produced: a list of file names, and a string for the GL-DancingMen font (the latter can be copied) +- Over HTTP the ciphertext can be saved as a PNG (not available over `file://`) + +### Decrypt tab + +- Click the figures in order, or select them with Tab and press Enter, to rebuild the plaintext +- A flagged figure ends a word, and no trailing space is left at the end of a line +- A font string or a list of file names can be pasted in and decrypted in one go +- The plaintext grows as you go, and single letters or the whole line can be removed + +### Substitution table tab + +- All 26 letters with both variants, flagged and unflagged +- Any figure enlarges in a native modal dialog, and focus returns to the button that opened it + +### Everything else + +- Japanese and English interface, switchable in one click and remembered for the next visit +- Responsive layout, usable on a phone +- Runs entirely in the browser: no build step, no external API, no analytics +- Tabs, decrypt buttons and the enlarged view are reachable with the keyboard alone + +--- + +## 📖 How to use + +Everything happens on the [demo page](https://ipusiron.github.io/dancingmen-cipherlab/); nothing has to be installed. + +### Encrypting + +1. Open the **Encrypt** tab. +2. Type a message (a-z, A-Z, spaces and line breaks). Full-width letters and spaces are folded to half-width. +3. The figures appear as you type. The **Encrypt** button does the same thing. +4. To try the original messages, pick one from the list and press **Put it in the input box**. +5. Copy the font string, or press **Save the ciphertext as a PNG** when the page is served over HTTP. + +The input is limited to 2,000 characters. +A note about removed or converted characters stays until the next edit that needs no correction. + +### Decrypting + +1. Open the **Decrypt** tab. +2. Click the figures in the order they appear in the ciphertext, or select them with Tab and press Enter. +3. The figures and the plaintext are appended below. +4. To decrypt from text, paste a font string or a list of file names and press **Decrypt the pasted ciphertext**. +5. **Delete one letter** removes the last figure, **Clear** resets both the figures and the paste box. + +In a font string, lower case means no flag and upper case means a flag. +A space alone does not separate words, so `am here` decrypts to `AMHERE` while `aM here` decrypts to `AM HERE`. + +### Reading the table + +1. Open the **Substitution table** tab. +2. Compare each letter with its two figures. +3. Click a figure, or select it with Tab and press Enter, to see it enlarged. +4. Esc or **Close** dismisses the dialog and returns focus to the button that opened it. + +While a tab has focus, the arrow keys move to the neighbouring tab and Home/End jump to the first or last one. +The figures shown in the Encrypt tab are not clickable. + +--- + +## 🚩 The flag rule + +A flag marks the end of a word. +The input is normalised first, and then two rules decide where flags go. + +1. A letter followed by a space carries a flag. +2. A letter at the end of a line carries a flag when the next line still contains at least one letter. + +A blank line, or a line of spaces, separates one message from the next; rule 2 does not reach across it. +Without a trailing space, the last letter of a message carries no flag. +In the table, `⏎` is a line break and `␣` is a space. + +| Input | Font string | Why | +|---|---|---| +| HELLO WORLD | hellO world | The last letter of a word takes the flag | +| HELLO WORLD | hellO world | A full-width space becomes a half-width one first | +| MEET ME⏎AT NOON | meeT mE⏎aT noon | A line that continues also flags its last word | +| HELLO⏎⏎WORLD | hello⏎⏎world | A blank line starts a new message, whose last word takes no flag | +| COME HERE AT ONCE␣ | comE herE aT oncE | A trailing space flags the very last letter | + +In [the original text (Project Gutenberg #108)](https://www.gutenberg.org/ebooks/108) the flags are taken to +divide a sentence into words, and a ciphertext without any flag is read as a single word. + +--- + +## 🏗️ Architecture + +### Stack + +- **Front end**: plain HTML5, CSS3 and vanilla JavaScript +- **Build**: none; the repository is the deployed site +- **Dependencies**: none +- **Hosting**: GitHub Pages + +### Design rules + +1. **Simplicity**: no build pipeline and no third-party library. +2. **Reach**: anyone can clone it and open it. +3. **Maintainability**: little code, arranged so that each file has one job. + +### How the parts divide + +- `dancingmen-logic.js` holds the cipher itself and knows nothing about the page +- `dancingmen-messages.js` holds every phrase, in Japanese and in English, under the same keys +- `i18n.js` picks the language, applies it to `data-i18n` markup and remembers the choice +- `script.js` handles input, tabs, drawing, the PNG export and the dialog +- A space is drawn as a flag on the preceding figure, which keeps the row of figures visually continuous +- Three SVG variants exist: full (display and PNG), padded (table and buttons) and tight (the source of full) + +--- + +## 🖋 About the font + +The figures come from the **GL-DancingMen** font published by +[Gutenberg Labo](https://github.com/Gutenberg-Labo/GL-DancingMen) (the file is `GL-DancingMen.ttf`). +Each glyph was rendered to its own SVG file, which is what the page displays. + +Only six short ciphertexts appear in the story, so the letters F, J, K, Q, U, W, X and Z never show up in it. +The figures for those eight letters are original designs by the font's author. + +To try the font itself, [fontspace](https://www.fontspace.com/gl-dancingmen-font-f12468) serves it directly: +lower case gives an unflagged figure, upper case a flagged one, and a space stays a space. + +### The three SVG variants + +| Variant | What it is | Used for | +|---|---|---| +| **full** | The tight variant with the viewBox widened by 5pt left and right and 2pt top and bottom | Rows of figures, and the PNG export | +| **tight** | The most closely cropped variant, generated with `bbox_inches='tight'` | The source the full variant is built from | +| **padded** | The first generation, 72x72pt with generous margins | Looking up or comparing a single letter | + +Both the padded and the tight files were produced from the TTF with Python and Matplotlib: the padded ones +centred in a 1x1 inch figure at 48 dpi, the tight ones anchored to the top-left in a 0.4x0.4 inch figure at +120 dpi with `pad_inches=0`. + +--- + +## 🔧 Implementation notes + +`sanitizeInput` normalises the text, `encryptText` turns it into rows of `{ letter, flag }` tokens, and +`toFileNameText` and `toFontText` render those rows in the two notations. +`parseCipherText` and `decodeTokens` take either notation back to plaintext. + +The tight SVGs let the flag stick out of the `viewBox` (up to 3.8pt to the right and 1.49pt upwards), so the +full variant widens the box and the figures are laid out at their intrinsic size. +A negative margin of -5pt brings the spacing back to the font's own advance width of 38.4px, and +`mix-blend-mode: multiply` drops the white background. + +The PNG export measures the rows with `layoutCipher` and draws the same full SVGs onto a canvas at twice the +scale. Anything wider or taller than 8,000px is refused. +`node tools/build-full-svg.js` regenerates the full variant, and `--check` compares without writing. + +Every phrase the page shows is looked up by key, so a language switch redraws the figures' alternative text, +the substitution table, the sample list and the status messages together. + +--- + +## 🔒 Security and privacy + +- A meta CSP allows scripts, styles and images from the same origin only. No inline handler and no `style` attribute is used +- The referrer is set to `no-referrer`, and links that open a new tab carry `rel="noopener noreferrer"` +- Input is normalised and length-limited, and everything is written through `textContent` +- No external API, CDN or analytics: displaying and using the page causes no outbound request +- When the clipboard API is unavailable or denied, the page explains how to copy by hand +- Nothing you type leaves the browser. The only thing stored locally is the chosen language + +Two limits are worth stating. Following an external link does talk to that site, and a meta CSP cannot stop +clickjacking, because HTTP response headers such as `X-Frame-Options` cannot be set from static files on +GitHub Pages. + +--- + +## 🧪 Tests + +Node.js 22 or later, from the repository root. There is nothing to install. + +```bash +npm test +node tools/build-full-svg.js --check +``` + +GitHub Actions runs `npm test` on every push and pull request. +The suite covers the flag rule, normalisation, round trips between the two notations, figure dimensions, PNG +layout, colour contrast, the HTML, both dictionaries and the tables in this file. +It also verifies with SHA-256 that the 104 original SVGs are untouched and that the 52 full SVGs still match +what the build script produces. + +--- + +## 🔎 Exploring the Dancing Men + +*"The Adventure of the Dancing Men"* is collected in *The Return of Sherlock Holmes*. +Six ciphertexts appear in it, and this tool can generate all of them. + +| No. | Plaintext | Font string (lower case = no flag, upper case = flag) | +|---|---|---| +| 1 | AM HERE ABE SLANEY | aM herE abE slaney | +| 2 | AT ELRIGES | aT elriges | +| 3 | COME ELSIE | comE elsie | +| 4 | NEVER | never | +| 5 | ELSIE PREPARE TO⏎MEET THY GOD | elsiE preparE tO⏎meeT thY god | +| 6 | COME HERE AT ONCE | comE herE aT once | + +Messages 1 to 5 agree with the notation listed in the +[ReadMe of GL-DancingMen-Org](https://github.com/Gutenberg-Labo/GL-DancingMen/blob/main/documents/GL-DancingMen-Org-readme.txt), +the font drawn after the original illustrations. +The fifth message occupies two lines in the book, and the O that ends its first line carries a flag. +The sixth is the one exception: the ReadMe gives `comE herE aT oncE`, with a flag on the final E as well. +To reproduce that here, add one trailing space to the input. The bundled sample has no trailing space. + +![The six ciphertexts of the story](assets/dancingmen_messages.png) + +> *All six messages, exported as a PNG by this tool. Blank lines separate them; only the fifth takes two lines.* + +The cipher is a simple monoalphabetic substitution, which is why frequency analysis works on it. Holmes +guesses that the most frequent figure is E: four of the fifteen figures in the first message are the same one. +He then works out that the flags mark word boundaries. + +For frequency analysis it helps to replace the figures with ordinary letters first, which is true of any +substitution cipher written in pictograms. + +👉 [Frequency Analyzer](https://github.com/ipusiron/frequency-analyzer) + +--- + +## 📁 Directory structure + +```text +dancingmen-cipherlab/ # A web tool for the Dancing Men cipher +├── .github/ # GitHub configuration +│ └── workflows/ # GitHub Actions workflows +│ └── test.yml # Runs npm test on push and pull request +├── .gitignore # Paths kept out of version control +├── .nojekyll # Disables Jekyll processing on Pages +├── assets/ # Figure SVGs and the images in the READMEs +│ ├── dancingmen_messages.png # The six original messages, exported by this tool +│ ├── screenshot.png # Encrypt tab, with the fifth message on two lines +│ ├── screenshot2.png # Decrypt tab, decrypting a pasted font string +│ ├── screenshot3.png # Substitution table tab +│ └── svg/ # Figures generated from the GL-DancingMen font +│ ├── full/ # 52 files widened by 5pt/2pt, for rows and PNGs +│ ├── padded/ # 52 files at 72x72pt, for the table and buttons +│ └── tight/ # 52 closely cropped files, the source of full +├── CLAUDE.md # Development guide for AI assistants +├── dancingmen-logic.js # The cipher itself, independent of the page +├── dancingmen-messages.js # Japanese and English phrases, and the formatter +├── i18n.js # Language choice and data-i18n application +├── index.html # The three tabs +├── LICENSE # MIT license of this tool +├── package.json # npm test, with no dependencies +├── README.en.md # This document +├── README.md # The Japanese document +├── script.js # Input, tabs, drawing, PNG export, dialog +├── style.css # Colour variables and the responsive layout +├── test/ # Automated tests for node --test +│ ├── assets.test.js # Names, sizes and integrity of the 156 SVGs +│ ├── contrast.test.js # Contrast of text against every surface +│ ├── format.test.js # Line length and source size +│ ├── html.test.js # CSP, ARIA and the absence of inline code +│ ├── i18n.test.js # Dictionaries, data-i18n keys and the switch +│ ├── layout.test.js # Figure dimensions, viewBox widening, PNG layout +│ ├── logic.test.js # Normalisation, flags, notations, round trips +│ ├── messages.test.js # The dictionary and the keys the page asks for +│ ├── readme.test.js # Tables, images, tree and metadata +│ ├── samples.test.js # The known answers of the six messages +│ └── static.test.js # Purity, forbidden constructs and the CI setup +└── tools/ # Development scripts, unused by the page + └── build-full-svg.js # Builds full from tight; --check only compares +``` + +--- + +## 💻 Requirements + +Static HTML, CSS and JavaScript, with no build step. +Encrypting, decrypting, the table and copying were checked on Chromium 145 on Windows, over HTTP and over +`file://`. Copying depends on the browser's permission settings; when it is refused, the page says so. +Firefox and Safari are untested. + +A local clone can be opened directly, except for the PNG export, which needs HTTP. + +```bash +git clone https://github.com/ipusiron/dancingmen-cipherlab.git +cd dancingmen-cipherlab +python -m http.server 8000 --bind 127.0.0.1 +``` + +Then open `http://127.0.0.1:8000/`. +Served that way, "COME HERE AT ONCE" exports as 1166x198 and all six messages as 1244x1502. + +--- + +## 📄 License + +MIT License. See [LICENSE](LICENSE) for the full text. + +The figures were generated from the +[GL-DancingMen font](https://github.com/Gutenberg-Labo/GL-DancingMen) +(Copyright (C) 2007-2009 Das Ende der Wildnis, Copyright (C) 2008-2023 Gutenberg Labo). +That font may be used, copied and redistributed freely, modified or not, for any purpose, without warranty. +See the font's [LICENSE.txt](https://github.com/Gutenberg-Labo/GL-DancingMen/blob/main/LICENSE.txt). + +--- + +## 🛠️ About this project + +This tool is part of **Security Tools 100 with Generative AI**, a series in which one security-related tool is +built and published each day with the help of generative AI. + +🔗 [https://akademeia.info/?page_id=42163](https://akademeia.info/?page_id=42163) diff --git a/README.md b/README.md index 14f3419..59bc08c 100644 --- a/README.md +++ b/README.md @@ -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) @@ -99,7 +101,7 @@ hub: true ### その他の機能 - レスポンシブデザイン対応 -- 日本語UI(シャーロキアン向け) +- 日本語UIと英語UIの切り替え(右上のボタン。選択はブラウザーに記憶され、`?lang=ja`・`?lang=en`でも指定可能) - 純粋なクライアントサイド実装(外部API不要) - タブ・復号ボタン・置換表の拡大をキーボードだけで操作可能 @@ -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 --- ## 🖋 使用フォントについて @@ -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`イベントが起き、人形の代替テキスト・置換表・サンプル一覧・状態メッセージを作り直します。 +入力欄の内容と復号中の人形は残ります。 --- ## 🔒 セキュリティ @@ -299,6 +307,7 @@ JavaScriptが表示する文言は`dancingmen-messages.js`の日本語辞書か - 入力の正規化と長さ制限を実施。入力・貼り付け・メッセージはtextContentで表示 - 外部API・CDN・アクセス解析を使用せず、ページの表示と操作で外部への通信は0件 - コピーAPIが使えない、または拒否された場合は手動コピーの案内を表示 +- 入力した文はブラウザーの外に出ません。`localStorage`に保存するのは言語の選択だけです 外部リンクをユーザーが開いたときは、そのリンク先へ通信します。 metaのCSPではクリックジャッキングを防げません。 @@ -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枚が生成スクリプトの出力と一致することも検証します。 --- @@ -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変数の配色とレスポンシブレイアウト @@ -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 # 文言の辞書と画面側のキーを検証 diff --git a/dancingmen-messages.js b/dancingmen-messages.js index 6ac4eea..c5d571c 100644 --- a/dancingmen-messages.js +++ b/dancingmen-messages.js @@ -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}文字を超えた分は切り捨てました", @@ -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" } }; @@ -45,7 +166,7 @@ const DancingMenMessages = (() => { }); } - return { DEFAULT_LANG, dictionaries, format }; + return { DEFAULT_LANG, LANGUAGES, dictionaries, format }; })(); globalThis.DancingMenMessages = DancingMenMessages; diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..753a71b --- /dev/null +++ b/i18n.js @@ -0,0 +1,78 @@ +// Language selection for the page. The wording itself lives in dancingmen-messages.js. +const I18n = (() => { + const messages = typeof DancingMenMessages === "object" + ? DancingMenMessages + : require("./dancingmen-messages.js"); + const STORAGE_KEY = "dancingmen-cipherlab-language"; + const ATTRIBUTES = ["aria-label", "title", "placeholder", "alt"]; + let language = messages.DEFAULT_LANG; + + function supported(value) { + return messages.LANGUAGES.includes(value); + } + + function t(key, values = {}) { + return messages.format(language, key, values); + } + + function apply(root = document) { + document.documentElement.lang = language; + document.title = t("app.title"); + for (const element of root.querySelectorAll("[data-i18n]")) { + element.textContent = t(element.dataset.i18n); + } + for (const attribute of ATTRIBUTES) { + for (const element of root.querySelectorAll(`[data-i18n-${attribute}]`)) { + element.setAttribute(attribute, t(element.getAttribute(`data-i18n-${attribute}`))); + } + } + } + + function setLanguage(value) { + if (!supported(value) || value === language) return; + language = value; + try { + localStorage.setItem(STORAGE_KEY, value); + } catch { + // Private modes reject storage. The choice then lasts for this visit only. + } + apply(); + document.dispatchEvent(new Event("languagechange")); + } + + function preferred() { + const query = new URLSearchParams(location.search).get("lang"); + let saved = null; + try { + saved = localStorage.getItem(STORAGE_KEY); + } catch { + // Without storage the browser setting decides. + } + const chosen = [query, saved].find(supported); + if (chosen) return chosen; + return /^ja\b/i.test(navigator.language || "") ? "ja" : "en"; + } + + function init() { + language = preferred(); + apply(); + } + + return { + STORAGE_KEY, + t, + apply, + init, + setLanguage, + supported, + get languages() { return [...messages.LANGUAGES]; }, + get ja() { return messages.dictionaries.ja; }, + get en() { return messages.dictionaries.en; }, + get language() { return language; } + }; +})(); + +globalThis.I18n = I18n; +if (typeof module === "object" && module.exports) { + module.exports = I18n; +} diff --git a/index.html b/index.html index 56b8fe4..8f56efb 100644 --- a/index.html +++ b/index.html @@ -10,33 +10,38 @@ - +
+
+ +

DancingMen CipherLab

-

シャーロック・ホームズの「踊る人形」暗号を体験できるツールです。

+

シャーロック・ホームズの「踊る人形」暗号を体験できるツールです。

-