Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- **Diagrams & rich content**: [KaTeX](https://katex.org/) math, [Mermaid](https://mermaid.js.org/), [Vega/Vega-Lite](https://vega.github.io/), [PlantUML](https://plantuml.com/), and [Prism](https://prismjs.com/) syntax highlighting in code blocks.
- **Markdown ↔ HTML** round-trip via [`marked`](https://github.com/markedjs/marked) (read path) and [`turndown`](https://github.com/mixmark-io/turndown) + `joplin-turndown-plugin-gfm` (write path). `MarkdownToHtml` is exposed as a standalone utility; output passes through [DOMPurify](https://github.com/cure53/DOMPurify) (`sanitizeHyperlink` + `isValidAttribute`) for XSS-safe rendering.
- **Search and replace** with regex support, plus undo/redo history.
- **i18n** out of the box: English, Chinese, and Japanese locales ship with the package.
- **i18n** out of the box: 9 locales ship with the package — English, Simplified and Traditional Chinese, Japanese, Korean, Spanish, French, German, Portuguese.
- **JSON state model** built on [`ot-json1`](https://github.com/ottypes/json1) / [`ot-text-unicode`](https://github.com/ottypes/text-unicode) — wire it up to your own transport for collaborative editing.
- **TypeScript first** — types ship in the package, no extra `@types/*` install needed.

Expand Down Expand Up @@ -48,7 +48,7 @@ import {
TableColumnToolbar,
TableDragBar,
TableRowColumMenu,
zh,
zhCN,
} from '@muyajs/core';

import '@muyajs/core/lib/style.css';
Expand Down Expand Up @@ -88,7 +88,7 @@ const muya = new Muya(container, {
});

// 3. Optional: switch the UI language before init.
muya.locale(zh);
muya.locale(zhCN);

// 4. Boot. Nothing renders until init() runs.
muya.init();
Expand All @@ -103,7 +103,7 @@ The `Muya` instance returned from `new Muya(el, options)` exposes:
| Method | Purpose |
| --- | --- |
| `init()` | Mount the editor and instantiate registered UI plugins. |
| `locale(localeObject)` | Switch the UI locale. Use the bundled `en`, `zh`, `ja` exports or supply your own. |
| `locale(localeObject)` | Switch the UI locale. Use one of the bundled exports (`en`, `zhCN`, `zhTW`, `ja`, `ko`, `es`, `fr`, `de`, `pt`) or supply your own. |
| `getMarkdown()` | Serialize the current document to Markdown. |
| `getState()` | Return the underlying JSON state (the source of truth). |
| `setContent(content, autoFocus?)` | Replace the document with Markdown (`string`) or `TState[]`. |
Expand Down
8 changes: 7 additions & 1 deletion e2e/host/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@
<div class="tools">
<select id="language-select">
<option value="en">English</option>
<option value="zh">中文</option>
<option value="zh-CN">简体中文</option>
<option value="zh-TW">繁體中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="pt">Português</option>
</select>
<button id="undo">Undo</button>
<button id="redo">Redo</button>
Expand Down
33 changes: 24 additions & 9 deletions e2e/host/main.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
/* eslint-disable antfu/no-top-level-await */
import type { IMuyaOptions, TState } from '@muyajs/core';
import type { ILocale, IMuyaOptions, TState } from '@muyajs/core';
import {
CodeBlockLanguageSelector,
de,
EmojiSelector,
en,
es,
FootnoteTool,
fr,
ImageEditTool,
ImageResizeBar,
ImageToolBar,
InlineFormatToolbar,
ja,
ko,
LinkTools,
MarkdownToHtml,
Muya,
ParagraphFrontButton,
ParagraphFrontMenu,
ParagraphQuickInsertMenu,
PreviewToolBar,
pt,
TableColumnToolbar,
TableDragBar,
TableRowColumMenu,
zh,
zhCN,
zhTW,
} from '@muyajs/core';
import './style.css';

Expand Down Expand Up @@ -130,14 +136,23 @@ window.__e2e = {
// Toolbar wiring (mirrors the buttons declared in index.html).
const $ = <T extends HTMLElement>(id: string): T => document.querySelector<T>(id)!;

// Keys MUST match the `value` attributes in #language-select (e2e/host/index.html).
const LOCALES: Record<string, ILocale> = {
'en': en,
'zh-CN': zhCN,
'zh-TW': zhTW,
'ja': ja,
'ko': ko,
'es': es,
'fr': fr,
'de': de,
'pt': pt,
};

$<HTMLSelectElement>('#language-select').addEventListener('change', (event) => {
const lang = (event.target as HTMLSelectElement).value;
if (lang === 'en')
muya.locale(en);
else if (lang === 'ja')
muya.locale(ja);
else if (lang === 'zh')
muya.locale(zh);
const locale = LOCALES[(event.target as HTMLSelectElement).value];
if (locale)
muya.locale(locale);
});

$<HTMLButtonElement>('#undo').addEventListener('click', () => muya.undo());
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/i18n/locale-switch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { expect, test } from '../fixtures/muya';
import { editor, floats, toolbar } from '../helpers/selectors';

test.describe('locale switch', () => {
test('switching to zh flips muya.i18n.lang', async ({ page }) => {
test('switching to zh-CN flips muya.i18n.lang', async ({ page }) => {
const initial = await page.evaluate(() => window.muya!.i18n.lang);
expect(initial).toBe('en');
await page.locator(toolbar.languageSelect).selectOption('zh');
await page.locator(toolbar.languageSelect).selectOption('zh-CN');
const after = await page.evaluate(() => window.muya!.i18n.lang);
expect(after).toBe('zh');
expect(after).toBe('zh-CN');
});

test('switching to ja flips muya.i18n.lang', async ({ page }) => {
Expand All @@ -17,7 +17,7 @@ test.describe('locale switch', () => {
});

test('locale change is reflected in slash menu item labels', async ({ page }) => {
await page.locator(toolbar.languageSelect).selectOption('zh');
await page.locator(toolbar.languageSelect).selectOption('zh-CN');
await page.evaluate(() => window.muya!.setContent(''));
await page.locator(editor.paragraph).first().click();
await page.keyboard.type('/');
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/smoke/public-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ test.describe('public api', () => {
test('locale switch flips muya.i18n.lang', async ({ page }) => {
const before = await page.evaluate(() => window.muya!.i18n.lang);
expect(before).toBe('en');
await page.locator('#language-select').selectOption('zh');
await page.locator('#language-select').selectOption('zh-CN');
const after = await page.evaluate(() => window.muya!.i18n.lang);
expect(after).toBe('zh');
expect(after).toBe('zh-CN');
});
});
8 changes: 7 additions & 1 deletion examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
<summary>Locale</summary>
<label for="language-select">Language:</label>
<select id="language-select">
<option value="zh">中文</option>
<option value="en">English</option>
<option value="zh-CN">简体中文</option>
<option value="zh-TW">繁體中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="pt">Português</option>
</select>
</details>

Expand Down
24 changes: 21 additions & 3 deletions examples/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@
import type { IMuyaOptions, TState } from '@muyajs/core';
import {
CodeBlockLanguageSelector,
de,
EmojiSelector,
en,
es,
FootnoteTool,
fr,
ImageEditTool,
ImageResizeBar,
ImageToolBar,
InlineFormatToolbar,
ja,
ko,
LinkTools,
MarkdownToHtml,
Muya,
ParagraphFrontButton,
ParagraphFrontMenu,
ParagraphQuickInsertMenu,
PreviewToolBar,
pt,
TableColumnToolbar,
TableDragBar,
TableRowColumMenu,
zh,
zhCN,
zhTW,
} from '@muyajs/core';

import { DEFAULT_MARKDOWN } from './data';
Expand All @@ -29,6 +35,7 @@ import './style.css';

// ---------- Firefox Intl.Segmenter polyfill ----------

// eslint-disable-next-line no-restricted-syntax -- structural widening over the const Intl namespace; alternative is augmenting global Intl which leaks polyfill semantics into every consumer
const intlNs = Intl as unknown as { Segmenter?: typeof Intl.Segmenter };
if (!intlNs.Segmenter) {
const polyfill = await import('intl-segmenter-polyfill/dist/bundled');
Expand Down Expand Up @@ -73,10 +80,21 @@ Muya.use(PreviewToolBar);

// ---------- Mutable runtime state ----------

const LOCALES = { zh, en, ja } as const;
// Keys MUST match the `value` attributes in #language-select (examples/index.html).
const LOCALES = {
'en': en,
'zh-CN': zhCN,
'zh-TW': zhTW,
'ja': ja,
'ko': ko,
'es': es,
'fr': fr,
'de': de,
'pt': pt,
} as const;
type TLocaleKey = keyof typeof LOCALES;

let currentLocale: TLocaleKey = 'zh';
let currentLocale: TLocaleKey = 'en';

// `satisfies` lets the literal stay structurally typed (Record<string,
// unknown>-like for the form-driven mutation below) while still failing
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { en, ja, zh } from './locales';
export type { ILocale } from './i18n/types';
export { de, en, es, fr, ja, ko, pt, zhCN, zhTW } from './locales';

export { Muya } from './muya';
export type { ITocItem } from './state/getTOC';
Expand Down
88 changes: 88 additions & 0 deletions packages/core/src/locales/de.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export const de = {
name: 'de',
resource: {
// tableTools
'Insert Row Above': 'Zeile oberhalb einfügen',
'Insert Row Below': 'Zeile unterhalb einfügen',
'Remove Row': 'Zeile entfernen',
// tableColumnTools
'Align Left': 'Linksbündig',
'Align Center': 'Zentriert',
'Align Right': 'Rechtsbündig',
'Insert Column left': 'Spalte links einfügen',
'Insert Column right': 'Spalte rechts einfügen',
'Remove Column': 'Spalte entfernen',
// quickInsert
'Paragraph': 'Absatz',
'Horizontal Line': 'Horizontale Linie',
'Front Matter': 'Front Matter',
'Header 1': 'Überschrift 1',
'Header 2': 'Überschrift 2',
'Header 3': 'Überschrift 3',
'Header 4': 'Überschrift 4',
'Header 5': 'Überschrift 5',
'Header 6': 'Überschrift 6',
'Table Block': 'Tabelle',
'Display Math': 'Mathematische Formel',
'HTML Block': 'HTML-Block',
'Code Block': 'Codeblock',
'Quote Block': 'Zitat',
'Order List': 'Nummerierte Liste',
'Bullet List': 'Aufzählung',
'To-do List': 'Aufgabenliste',
'Vega Chart': 'Vega-Diagramm',
'Mermaid': 'Mermaid',
'Plantuml': 'Plantuml',
'basic blocks': 'Grundblöcke',
'headers': 'Überschriften',
'advanced blocks': 'Erweiterte Blöcke',
'list blocks': 'Listen',
'diagrams': 'Diagramme',
'No result': 'Kein Ergebnis',
'Search keyword...': 'Suchbegriff...',
'Type / to insert...': '/ eingeben zum Einfügen...',
// formatPicker
'Emphasize': 'Fett',
'Italic': 'Kursiv',
'Underline': 'Unterstrichen',
'Strikethrough': 'Durchgestrichen',
'Highlight': 'Hervorheben',
'Inline Code': 'Inline-Code',
'Inline Math': 'Inline-Formel',
'Link': 'Link',
'Image': 'Bild',
'Eliminate': 'Formatierung entfernen',
// Code block
'Copy content': 'Inhalt kopieren',
'Input Language Identifier...': 'Sprachkennung eingeben...',
// emojiPicker
'Smileys & Emotion': 'Smileys & Emotionen',
'People & Body': 'Menschen & Körper',
'Animals & Nature': 'Tiere & Natur',
'Food & Drink': 'Essen & Trinken',
'Travel & Places': 'Reisen & Orte',
'Activities': 'Aktivitäten',
'Objects': 'Objekte',
'Symbols': 'Symbole',
'Flags': 'Flaggen',
// frontMenu
'Duplicate': 'Duplizieren',
'New Paragraph': 'Neuer Absatz',
'Delete': 'Löschen',
// imageToolbar
'Edit Image': 'Bild bearbeiten',
'Inline Image': 'Inline-Bild',
'Remove Image': 'Bild entfernen',
// ImageSelector
'Image src placeholder': 'Bild-URL',
'Confirm Text': 'OK',
// preview block
'Loading...': 'Wird geladen...',
'Invalid Diagram Code': 'Ungültiger Diagramm-Code',
'Empty Diagram': 'Leeres Diagramm',
'Input Mathematical Formula...': 'Mathematische Formel eingeben...',
'Input Front Matter...': 'Front Matter eingeben...',
'Invalid Mathematical Formula': 'Ungültige mathematische Formel',
'Empty Mathematical Formula': 'Leere mathematische Formel',
},
};
Loading
Loading