diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a02022c3..e7a9ec21 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "dictionary-fr": "^3.0.0", "dictionary-it": "^2.0.0", "dictionary-nb": "^3.0.0", + "dictionary-nl": "^2.0.0", "fflate": "^0.8.3", "nspell": "^2.1.5", "svelte-i18n": "^4.0.1", @@ -3445,6 +3446,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/dictionary-nl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dictionary-nl/-/dictionary-nl-2.0.0.tgz", + "integrity": "sha512-BJRu3jT52mEjDunbpC9Y1UiQJy22VUuA/N6s2Oc5KuN+hHS3MDCoTQiyjifePd2rlHtZSy0fGZaB0qXiCGh7Vw==", + "license": "(BSD-3-Clause OR CC-BY-3.0)", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8bccb7a6..beafda73 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,7 @@ "dev": "vite", "prebuild": "node scripts/copy-dictionaries.mjs", "build": "vite build", + "test": "node --test scripts/*.test.mjs", "preview": "vite preview", "check": "svelte-check --tsconfig ./tsconfig.json", "lint": "eslint .", @@ -41,6 +42,7 @@ "dictionary-fr": "^3.0.0", "dictionary-it": "^2.0.0", "dictionary-nb": "^3.0.0", + "dictionary-nl": "^2.0.0", "fflate": "^0.8.3", "nspell": "^2.1.5", "svelte-i18n": "^4.0.1", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index eb195a77..2290a20b 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -48a90fd1c3ac1a9e4f4707bb63114d36 \ No newline at end of file +41981e805c2980f748c8242e519ab085 \ No newline at end of file diff --git a/frontend/scripts/copy-dictionaries.mjs b/frontend/scripts/copy-dictionaries.mjs index 90f7e976..22b6492c 100644 --- a/frontend/scripts/copy-dictionaries.mjs +++ b/frontend/scripts/copy-dictionaries.mjs @@ -13,9 +13,9 @@ const scriptsDir = dirname(fileURLToPath(import.meta.url)) const frontend = dirname(scriptsDir) const outDir = join(frontend, 'public', 'spellcheck') -// Latin-script locales Aerion ships translations for. zh-* excluded (CJK has no +// Latin-script locales Aerion ships dictionaries for. zh-* excluded (CJK has no // Latin-style per-word spelling); vi omitted until a usable dictionary exists. -const LOCALES = ['en', 'cs', 'de', 'fr', 'it', 'nb'] +const LOCALES = ['en', 'cs', 'de', 'fr', 'it', 'nb', 'nl'] await rm(outDir, { recursive: true, force: true }) await mkdir(outDir, { recursive: true }) diff --git a/frontend/scripts/spellcheck.test.mjs b/frontend/scripts/spellcheck.test.mjs new file mode 100644 index 00000000..a1404695 --- /dev/null +++ b/frontend/scripts/spellcheck.test.mjs @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import test from 'node:test' +import nspell from 'nspell' +import { gunzipSync } from 'node:zlib' + +async function dutchDictionary() { + const [aff, dic] = await Promise.all([ + readFile(new URL('../node_modules/dictionary-nl/index.aff', import.meta.url), 'utf8'), + readFile(new URL('../node_modules/dictionary-nl/index.dic', import.meta.url), 'utf8'), + ]) + return nspell(aff, dic) +} + +test('Dutch dictionary recognizes Dutch words and rejects a typo', async () => { + const spell = await dutchDictionary() + + assert.equal(spell.correct('huis'), true) + assert.equal(spell.correct('huiss'), false) + assert.ok(spell.suggest('huiss').includes('huis')) +}) + +test('Dutch browser assets contain the dictionary files', async () => { + const [aff, dic] = await Promise.all([ + readFile(new URL('../public/spellcheck/nl.aff.gz', import.meta.url)), + readFile(new URL('../public/spellcheck/nl.dic.gz', import.meta.url)), + ]) + + assert.match(gunzipSync(aff).toString('utf8'), /Dutch support for Hunspell/) + assert.match(gunzipSync(dic).toString('utf8'), /\nhuis-\n/) +}) diff --git a/frontend/src/lib/components/settings/ComposerTab.svelte b/frontend/src/lib/components/settings/ComposerTab.svelte index 0ea964ed..b75536aa 100644 --- a/frontend/src/lib/components/settings/ComposerTab.svelte +++ b/frontend/src/lib/components/settings/ComposerTab.svelte @@ -43,7 +43,7 @@ // Dictionaries Aerion ships, with their native display names. const dictLanguages = SPELLCHECK_DICTS.map((code) => ({ code, - name: supportedLocales.find((l) => l.code === code)?.name ?? code, + name: supportedLocales.find((l) => l.code === code)?.name ?? (code === 'nl' ? 'Nederlands' : code), })) // Added-words list — read straight from the store (persists immediately on diff --git a/frontend/src/lib/spellcheck/locales.ts b/frontend/src/lib/spellcheck/locales.ts index eb325bb1..373c90e6 100644 --- a/frontend/src/lib/spellcheck/locales.ts +++ b/frontend/src/lib/spellcheck/locales.ts @@ -10,10 +10,11 @@ const APP_TO_DICT: Record = { fr: 'fr', it: 'it', nb: 'nb', + nl: 'nl', } // Dictionaries bundled as static assets under /spellcheck/.{aff,dic}. -export const SPELLCHECK_DICTS = ['en', 'cs', 'de', 'fr', 'it', 'nb'] as const +export const SPELLCHECK_DICTS = ['en', 'cs', 'de', 'fr', 'it', 'nb', 'nl'] as const export function appLocaleToDict(locale: string | null | undefined): string | null { if (!locale) return null