يكمن التحدي fundamental في استخدام CSpell مع Next.js في بنية إطار العمل نفسه، إذ يفصل الشيفرة إلى بيئتين مختلفتين: الخادم (Node.js) والعميل (المتصفح). وCSpell مكتبة مبنية على Node.js وتحتاج إلى وصول مباشر إلى نظام الملفات لقراءة ملفات القواميس، وهي قدرة لا يمتلكها المتصفح ببساطة.
ولهذا السبب، تنتهي محاولات استخدام CSpell في تطبيق Next.js افتراضيًا بأخطاء مثل "Module not found: Can't resolve 'fs'". فالمتصفح لا يستطيع العثور على وحدة fs الخاصة بـ Node.js أو الوصول إليها، وهي وحدة أساسية لتحميل القواميس.
الحل من جهة الخادم
الطريقة الأكثر موثوقية لدمج CSpell هي تشغيله على الخادم حصريًا. وفي تطبيق Next.js، يعني ذلك وضع منطق CSpell داخل API Route أو Server Component. وهذه ملفات خاصة يُضمن تنفيذها على الخادم فقط، ما يمنحها إمكانية الوصول إلى نظام ملفات Node.js وغيره من موارد الواجهة الخلفية.
ومن خلال إنشاء فئة مساعدة مثل SpellChecker الموضحة أدناه، يمكنك تجميع كل منطق التدقيق الإملائي في مكان واحد. تتولى هذه الفئة تحميل القواميس بشكل غير متزامن، وتوفر واجهة API واضحة لفحص النصوص، بما يضمن تنفيذ المهام الثقيلة دائمًا على الخادم.
حل المسارات: ضمان العثور على القواميس
من الأخطاء الشائعة أن CSpell قد يعجز عن تحديد مكان ملفات القواميس حتى على الخادم. ويحدث هذا غالبًا في بيئات النشر التي تختلف فيها بنية الملفات عن جهاز التطوير المحلي. والحل هو تحديد مسارات صريحة ومطلقة لملفات القواميس.
تستخدم الشيفرة المرفقة دالة مساعدة هي getDictionaryDefinitions، والتي تعتمد على path.join(process.cwd(), 'node_modules/...') لبناء مسار ملف موثوق.
تُرجع process.cwd() مجلد العمل الحالي، وهو جذر مشروع Next.js الخاص بك.
أما path.join(...)فتدمج المسارات بأمان، لتنشئ مسارًا مطلقًا يعمل باتساق عبر البيئات المختلفة.
وبهذا يعرف المدقق الإملائي دائمًا أين يجد القواميس، ما يجعل تطبيقك موثوقًا وقابلًا للنقل.
إليك فئة SpellChecker كاملة:
// lib/spell-checker.ts
import { spellCheckDocument, Document, SpellCheckFileOptions, CSpellUserSettings } from "cspell-lib";
import path from "path";
/**
* Creates and returns the SpellCheckerSettings object with explicit paths to dictionaries.
* This is a more robust approach for Next.js environments.
* @param locale The language locale to use.
* @returns The SpellCheckerSettings object.
*/
function getDictionaryDefinitions(locale: string): CSpellUserSettings {
const commonSettings: CSpellUserSettings = {
words: [],
suggestionsTimeout: 10000,
dictionaryDefinitions: [],
};
if (locale === 'ar') {
// You would need to install this package for the path to exist
// npm install @cspell/dict-ar
commonSettings.dictionaries = ['ar'];
commonSettings.dictionaryDefinitions = [
{
name: 'ar',
path: path.join(process.cwd(), 'node_modules/@cspell/dict-ar/ar.trie.gz'),
}
];
} else {
// Default to English if locale is not 'ar'
commonSettings.dictionaries = ['en_us', 'en-gb', 'en-us-common-misspellings', 'en-gb-common-misspellings'];
commonSettings.dictionaryDefinitions = [
{
name: 'en_us',
path: path.join(process.cwd(), 'node_modules/@cspell/dict-en_us/en_US.trie.gz'),
},
{
name: 'en-gb',
path: path.join(process.cwd(), 'node_modules/@cspell/dict-en_gb/en_GB.trie.gz'),
},
{
name: 'en-us-common-misspellings',
path: path.join(process.cwd(), 'node_modules/@cspell/dict-en-us-common-misspellings/dist/dict-en-us.json'),
},
{
name: 'en-gb-common-misspellings',
path: path.join(process.cwd(), 'node_modules/@cspell/dict-en-gb-common-misspellings/dist/dict-en-gb.json'),
}
];
}
return commonSettings;
}
/**
* The SpellChecker class for performing spell checking.
* It uses a static factory method to handle asynchronous dictionary loading.
*/
export class SpellChecker {
private locale = "en";
/**
* An async factory method to create and initialize a SpellChecker instance.
* With cspell-lib's 'spellCheckDocument' function, we don't need to load
* dictionaries into a stateful instance. This method simply maintains the
* requested class-based API.
* @returns A promise that resolves to a SpellChecker instance.
*/
public static async create(locale: string): Promise<SpellChecker> {
return new SpellChecker(locale);
}
// The constructor is private to enforce using the async factory method.
private constructor(locale = "en") {
this.locale = locale;
}
/**
* Checks if a single word is spelled correctly.
* @param word The word to check.
* @returns A promise that resolves to true if the word is correct, otherwise false.
*/
public async isCorrect(word: string): Promise<boolean> {
const doc: Document = {
uri: "text.txt",
text: word,
languageId: "plaintext",
locale: this.locale,
};
const checkOptions: CheckOptions = {
generateSuggestions: false,
noConfigSearch: true,
};
const settings: CSpellUserSettings = {
...getDictionaryDefinitions(this.locale),
};
const result = await spellCheckDocument(doc, checkOptions, settings);
return result.issues.length === 0;
}
/**
* Gets spelling suggestions for a misspelled word.
* @param word The misspelled word.
* @returns A promise that resolves to an array of suggested correct spellings.
*/
public async suggest(word: string): Promise<string[]> {
const doc: Document = {
uri: "text.txt",
text: word,
languageId: "plaintext",
locale: this.locale,
};
const checkOptions: CheckOptions = {
generateSuggestions: true,
noConfigSearch: true,
};
const settings: CSpellUserSettings = {
...getDictionaryDefinitions(this.locale),
suggestionsTimeout: 2000,
};
const result = await spellCheckDocument(doc, checkOptions, settings);
if (result.issues.length > 0) {
return result.issues[0].suggestions || [];
}
return [];
}
/**
* Checks an entire text for spelling errors.
* @param text The text to check.
* @returns A promise that resolves to an array of objects,
* each containing a misspelled word and its suggestions.
*/
public async checkText(text: string): Promise<{ word: string; suggestions: string[] }[]> {
const doc: Document = {
uri: "text.txt",
text: text,
languageId: "plaintext",
locale: this.locale,
};
const checkOptions: SpellCheckFileOptions = {
generateSuggestions: true,
noConfigSearch: true,
};
const settings: CSpellUserSettings = {
...getDictionaryDefinitions(this.locale),
suggestionsTimeout: 10000,
numSuggestions: 5,
ignoreRegExpList: [/\bhttps?:\/\/\S+\b/g, /\bwww\.\S+\b/g, /\b\d+\b/g, /[#@]\w+/g],
};
const errors: { word: string; suggestions: string[] }[] = [];
const result = await spellCheckDocument(doc, checkOptions, settings);
for (const issue of result.issues) {
errors.push({
word: issue.text,
suggestions: issue.suggestions || [],
});
}
return errors;
}
}