chris bdafd3f2c3 Self-host LanguageTool in Docker Compose
- Add erikvl87/languagetool service on the internal network
  (port 8010, English only by default — change langsToLoad to add more)
- 256 MB min / 512 MB max heap; healthcheck waits up to ~3 min for
  first startup while Java initialises
- Server LANGUAGETOOL_URL hardwired to http://languagetool:8010/v2 —
  no character limits, no rate limits, fully private
- Remove 40 000-char cap from lint route (not needed self-hosted)
- Bump fetch timeout to 30 s for cold-start requests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:43:43 -04:00

55 lines
1.8 KiB
JavaScript

import { Router } from 'express'
import { auth } from '../middleware/auth.js'
// Point at a self-hosted LanguageTool instance by setting LANGUAGETOOL_URL.
// Falls back to the free public API (rate-limited but good for personal use).
const LT_URL = (process.env.LANGUAGETOOL_URL || 'https://api.languagetoolplus.com/v2').replace(/\/$/, '')
const router = Router()
router.use(auth)
router.post('/check', async (req, res) => {
const { text, language = 'en-US' } = req.body || {}
if (!text || typeof text !== 'string') return res.status(400).json({ error: 'text required' })
try {
const body = new URLSearchParams({
text,
language,
// Suppress cosmetic rules that aren't useful in a creative-writing context
disabledRules: 'WHITESPACE_RULE,WORD_CONTAINS_UPPERCASE,EN_UNPAIRED_BRACKETS,DASH_RULE',
})
const r = await fetch(`${LT_URL}/check`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
body: body.toString(),
signal: AbortSignal.timeout(30000),
})
if (!r.ok) {
const msg = await r.text().catch(() => '')
throw new Error(`LanguageTool ${r.status}: ${msg.slice(0, 200)}`)
}
const data = await r.json()
// Send only what the client needs — keep the payload small
res.json({
matches: (data.matches || []).map(m => ({
message: m.message,
offset: m.offset,
length: m.length,
replacements: (m.replacements || []).slice(0, 6).map(r => r.value),
kind: m.rule?.category?.id === 'TYPOS' ? 'spelling' : 'grammar',
ruleId: m.rule?.id ?? '',
})),
})
} catch (err) {
console.error('[lint] LanguageTool error:', err.message)
res.status(502).json({ error: err.message })
}
})
export default router