diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp
index 5479c080bd827d49af52f04a565a224ea5e5c7ba..42cfc4cae9a37702bb5c15d1a0cca5a6a141c057 100644
--- a/indra/llui/lllineeditor.cpp
+++ b/indra/llui/lllineeditor.cpp
@@ -66,7 +66,7 @@ const S32	SCROLL_INCREMENT_ADD = 0;	// make space for typing
 const S32   SCROLL_INCREMENT_DEL = 4;	// make space for baskspacing
 const F32   AUTO_SCROLL_TIME = 0.05f;
 const F32	TRIPLE_CLICK_INTERVAL = 0.3f;	// delay between double and triple click. *TODO: make this equal to the double click interval?
-const F32	SPELLCHECK_DELAY = 0.5f;	// delay between the last keypress and showing spell checking feedback for the word the cursor is on
+const F32	SPELLCHECK_DELAY = 0.5f;	// delay between the last keypress and spell checking the word the cursor is on
 
 const std::string PASSWORD_ASTERISK( "\xE2\x80\xA2" ); // U+2022 BULLET
 
@@ -617,6 +617,7 @@ bool LLLineEditor::isMisspelledWord(U32 pos) const
 void LLLineEditor::onSpellCheckSettingsChange()
 {
 	// Recheck the spelling on every change
+	mMisspellRanges.clear();
 	mSpellCheckStart = mSpellCheckEnd = -1;
 }
 
@@ -1158,9 +1159,8 @@ void LLLineEditor::cut()
 			LLUI::reportBadKeystroke();
 		}
 		else
-		if( mKeystrokeCallback )
 		{
-			mKeystrokeCallback( this );
+			onKeystroke();
 		}
 	}
 }
@@ -1294,9 +1294,8 @@ void LLLineEditor::pasteHelper(bool is_primary)
 				LLUI::reportBadKeystroke();
 			}
 			else
-			if( mKeystrokeCallback )
 			{
-				mKeystrokeCallback( this );
+				onKeystroke();
 			}
 		}
 	}
@@ -1549,10 +1548,7 @@ BOOL LLLineEditor::handleKeyHere(KEY key, MASK mask )
 			// Notify owner if requested
 			if (!need_to_rollback && handled)
 			{
-				if (mKeystrokeCallback)
-				{
-					mKeystrokeCallback(this);
-				}
+				onKeystroke();
 				if ( (!selection_modified) && (KEY_BACKSPACE == key) )
 				{
 					mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
@@ -1608,12 +1604,10 @@ BOOL LLLineEditor::handleUnicodeCharHere(llwchar uni_char)
 		// Notify owner if requested
 		if( !need_to_rollback && handled )
 		{
-			if( mKeystrokeCallback )
-			{
-				// HACK! The only usage of this callback doesn't do anything with the character.
-				// We'll have to do something about this if something ever changes! - Doug
-				mKeystrokeCallback( this );
-			}
+			// HACK! The only usage of this callback doesn't do anything with the character.
+			// We'll have to do something about this if something ever changes! - Doug
+			onKeystroke();
+
 			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 		}
 	}
@@ -1643,9 +1637,7 @@ void LLLineEditor::doDelete()
 
 			if (!prevalidateInput(text_to_delete))
 			{
-				if( mKeystrokeCallback )
-					mKeystrokeCallback( this );
-
+				onKeystroke();
 				return;
 			}
 			setCursor(getCursor() + 1);
@@ -1661,10 +1653,8 @@ void LLLineEditor::doDelete()
 		}
 		else
 		{
-			if( mKeystrokeCallback )
-			{
-				mKeystrokeCallback( this );
-			}
+			onKeystroke();
+
 			mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 		}
 	}
@@ -2296,6 +2286,15 @@ void LLLineEditor::setSelectAllonFocusReceived(BOOL b)
 	mSelectAllonFocusReceived = b;
 }
 
+void LLLineEditor::onKeystroke()
+{
+	if (mKeystrokeCallback)
+	{
+		mKeystrokeCallback(this);
+	}
+
+	mSpellCheckStart = mSpellCheckEnd = -1;
+}
 
 void LLLineEditor::setKeystrokeCallback(callback_t callback, void* user_data)
 {
@@ -2418,10 +2417,8 @@ void LLLineEditor::updatePreedit(const LLWString &preedit_string,
 
 	// Update of the preedit should be caused by some key strokes.
 	mKeystrokeTimer.reset();
-	if( mKeystrokeCallback )
-	{
-		mKeystrokeCallback( this );
-	}
+	onKeystroke();
+
 	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 }
 
@@ -2575,6 +2572,7 @@ void LLLineEditor::showContextMenu(S32 x, S32 y)
 		S32 screen_x, screen_y;
 		localPointToScreen(x, y, &screen_x, &screen_y);
 
+		setCursorAtLocalPos(x);
 		if (hasSelection())
 		{
 			if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) )
@@ -2582,10 +2580,6 @@ void LLLineEditor::showContextMenu(S32 x, S32 y)
 			else
 				setCursor(llmax(mSelectionStart, mSelectionEnd));
 		}
-		else
-		{
-			setCursorAtLocalPos(x);
-		}
 
 		bool use_spellcheck = getSpellCheck(), is_misspelled = false;
 		if (use_spellcheck)
diff --git a/indra/llui/lllineeditor.h b/indra/llui/lllineeditor.h
index 9513274f21d53401a0f0f3b871fee63bfb94c397..40f931ecc1e9b0d08fefa7d8a85c1f9b1f7e1b5d 100644
--- a/indra/llui/lllineeditor.h
+++ b/indra/llui/lllineeditor.h
@@ -243,6 +243,7 @@ class LLLineEditor
 	void			setSelectAllonFocusReceived(BOOL b);
 	void			setSelectAllonCommit(BOOL b) { mSelectAllonCommit = b; }
 	
+	void			onKeystroke();
 	typedef boost::function<void (LLLineEditor* caller, void* user_data)> callback_t;
 	void			setKeystrokeCallback(callback_t callback, void* user_data);
 
diff --git a/indra/llui/llspellcheck.cpp b/indra/llui/llspellcheck.cpp
index 433ca0285243a68c25312a02e154203e4520f7d3..65207144f893efb8fb357edff5ae75f71e151d54 100644
--- a/indra/llui/llspellcheck.cpp
+++ b/indra/llui/llspellcheck.cpp
@@ -186,7 +186,7 @@ void LLSpellChecker::addToDictFile(const std::string& dict_path, const std::stri
 	}
 }
 
-void LLSpellChecker::setSecondaryDictionaries(std::list<std::string> dict_list)
+void LLSpellChecker::setSecondaryDictionaries(dict_list_t dict_list)
 {
 	if (!getUseSpellCheck())
 	{
@@ -194,11 +194,11 @@ void LLSpellChecker::setSecondaryDictionaries(std::list<std::string> dict_list)
 	}
 
 	// Check if we're only adding secondary dictionaries, or removing them
-	std::list<std::string> dict_add(llmax(dict_list.size(), mDictSecondary.size())), dict_rem(llmax(dict_list.size(), mDictSecondary.size()));
+	dict_list_t dict_add(llmax(dict_list.size(), mDictSecondary.size())), dict_rem(llmax(dict_list.size(), mDictSecondary.size()));
 	dict_list.sort();
 	mDictSecondary.sort();
-	std::list<std::string>::iterator end_added = std::set_difference(dict_list.begin(), dict_list.end(), mDictSecondary.begin(), mDictSecondary.end(), dict_add.begin());
-	std::list<std::string>::iterator end_removed = std::set_difference(mDictSecondary.begin(), mDictSecondary.end(), dict_list.begin(), dict_list.end(), dict_rem.begin());
+	dict_list_t::iterator end_added = std::set_difference(dict_list.begin(), dict_list.end(), mDictSecondary.begin(), mDictSecondary.end(), dict_add.begin());
+	dict_list_t::iterator end_removed = std::set_difference(mDictSecondary.begin(), mDictSecondary.end(), dict_list.begin(), dict_list.end(), dict_rem.begin());
 
 	if (end_removed != dict_rem.begin())		// We can't remove secondary dictionaries so we need to recreate the Hunspell instance
 	{
@@ -211,7 +211,7 @@ void LLSpellChecker::setSecondaryDictionaries(std::list<std::string> dict_list)
 	{
 		const std::string app_path = getDictionaryAppPath();
 		const std::string user_path = getDictionaryUserPath();
-		for (std::list<std::string>::const_iterator it_added = dict_add.begin(); it_added != end_added; ++it_added)
+		for (dict_list_t::const_iterator it_added = dict_add.begin(); it_added != end_added; ++it_added)
 		{
 			const LLSD dict_entry = getDictionaryData(*it_added);
 			if ( (!dict_entry.isDefined()) || (!dict_entry["installed"].asBoolean()) )
@@ -287,7 +287,7 @@ void LLSpellChecker::initHunspell(const std::string& dict_name)
 			}
 		}
 
-		for (std::list<std::string>::const_iterator it = mDictSecondary.begin(); it != mDictSecondary.end(); ++it)
+		for (dict_list_t::const_iterator it = mDictSecondary.begin(); it != mDictSecondary.end(); ++it)
 		{
 			const LLSD dict_entry = getDictionaryData(*it);
 			if ( (!dict_entry.isDefined()) || (!dict_entry["installed"].asBoolean()) )
diff --git a/indra/llui/llspellcheck.h b/indra/llui/llspellcheck.h
index affdac2907b4546450339a58cec2c756fde19ce5..8351655b49abaf1ccfca8cc58c5b782f9ad67fa4 100644
--- a/indra/llui/llspellcheck.h
+++ b/indra/llui/llspellcheck.h
@@ -44,17 +44,20 @@ class LLSpellChecker : public LLSingleton<LLSpellChecker>
 	void addToIgnoreList(const std::string& word);
 	bool checkSpelling(const std::string& word) const;
 	S32  getSuggestions(const std::string& word, std::vector<std::string>& suggestions) const;
-
-public:
-	const LLSD	getDictionaryData(const std::string& dict_name) const;
-	const LLSD&	getDictionaryMap() const { return mDictMap; }
-	void		refreshDictionaryMap();
-	void		setSecondaryDictionaries(std::list<std::string> dictList);
 protected:
-	void		addToDictFile(const std::string& dict_path, const std::string& word);
-	void		initHunspell(const std::string& dict_name);
+	void addToDictFile(const std::string& dict_path, const std::string& word);
+	void initHunspell(const std::string& dict_name);
 
 public:
+	typedef std::list<std::string> dict_list_t;
+
+	const std::string&	getActiveDictionary() const { return mDictName; }
+	const LLSD			getDictionaryData(const std::string& dict_name) const;
+	const LLSD&			getDictionaryMap() const { return mDictMap; }
+	const dict_list_t&	getSecondaryDictionaries() const { return mDictSecondary; }
+	void				refreshDictionaryMap();
+	void				setSecondaryDictionaries(dict_list_t dict_list);
+
 	static const std::string getDictionaryAppPath();
 	static const std::string getDictionaryUserPath();
 	static bool				 getUseSpellCheck();
@@ -64,11 +67,11 @@ class LLSpellChecker : public LLSingleton<LLSpellChecker>
 	static boost::signals2::connection setSettingsChangeCallback(const settings_change_signal_t::slot_type& cb);
 
 protected:
-	Hunspell*				 mHunspell;
-	std::string				 mDictName;
-	std::string				 mDictFile;
-	LLSD					 mDictMap;
-	std::list<std::string>	 mDictSecondary;
+	Hunspell*	mHunspell;
+	std::string	mDictName;
+	std::string	mDictFile;
+	LLSD		mDictMap;
+	dict_list_t	mDictSecondary;
 	std::vector<std::string> mIgnoreList;
 
 	static settings_change_signal_t	sSettingsChangeSignal;
diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp
index 0040be45c711cde660eb72359177e87e4571ac56..2231d9b983b1fea6e31641873c02c5f8ef21e35a 100644
--- a/indra/llui/lltextbase.cpp
+++ b/indra/llui/lltextbase.cpp
@@ -32,6 +32,7 @@
 #include "lllocalcliprect.h"
 #include "llmenugl.h"
 #include "llscrollcontainer.h"
+#include "llspellcheck.h"
 #include "llstl.h"
 #include "lltextparser.h"
 #include "lltextutil.h"
@@ -155,6 +156,7 @@ LLTextBase::Params::Params()
 	plain_text("plain_text",false),
 	track_end("track_end", false),
 	read_only("read_only", false),
+	spellcheck("spellcheck", false),
 	v_pad("v_pad", 0),
 	h_pad("h_pad", 0),
 	clip("clip", true),
@@ -181,6 +183,9 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)
 	mFontShadow(p.font_shadow),
 	mPopupMenu(NULL),
 	mReadOnly(p.read_only),
+	mSpellCheck(p.spellcheck),
+	mSpellCheckStart(-1),
+	mSpellCheckEnd(-1),
 	mCursorColor(p.cursor_color),
 	mFgColor(p.text_color),
 	mBorderVisible( p.border_visible ),
@@ -246,6 +251,12 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)
 		addChild(mDocumentView);
 	}
 
+	if (mSpellCheck)
+	{
+		LLSpellChecker::setSettingsChangeCallback(boost::bind(&LLTextBase::onSpellCheckSettingsChange, this));
+	}
+	mSpellCheckTimer.reset();
+
 	createDefaultSegment();
 
 	updateRects();
@@ -530,8 +541,86 @@ void LLTextBase::drawText()
 		return;
 	}
 
+	// Perform spell check if needed
+	if ( (getSpellCheck()) && (getWText().length() > 2) )
+	{
+		// Calculate start and end indices for the spell checking range
+		S32 start = line_start, end = getLineEnd(last_line);
+
+		if ( (mSpellCheckStart != start) || (mSpellCheckEnd != end) )
+		{
+			const LLWString& wstrText = getWText(); 
+			mMisspellRanges.clear();
+
+			segment_set_t::iterator seg_it = getSegIterContaining(start);
+			while (mSegments.end() != seg_it)
+			{
+				LLTextSegmentPtr text_segment = *seg_it;
+				if ( (text_segment.isNull()) || (text_segment->getStart() >= end) )
+				{
+					break;
+				}
+
+				if (!text_segment->canEdit())
+				{
+					++seg_it;
+					continue;
+				}
+
+				// Combine adjoining text segments into one
+				U32 seg_start = text_segment->getStart(), seg_end = llmin(text_segment->getEnd(), end);
+				while (mSegments.end() != ++seg_it)
+				{
+					text_segment = *seg_it;
+					if ( (text_segment.isNull()) || (!text_segment->canEdit()) || (text_segment->getStart() >= end) )
+					{
+						break;
+					}
+					seg_end = llmin(text_segment->getEnd(), end);
+				}
+
+				// Find the start of the first word
+				U32 word_start = seg_start, word_end = -1;
+				while ( (word_start < wstrText.length()) && (!LLStringOps::isAlpha(wstrText[word_start])) )
+					word_start++;
+
+				// Iterate over all words in the text block and check them one by one
+				while (word_start < seg_end)
+				{
+					// Find the end of the current word (special case handling for "'" when it's used as a contraction)
+					word_end = word_start + 1;
+					while ( (word_end < seg_end) && 
+							((LLWStringUtil::isPartOfWord(wstrText[word_end])) ||
+								((L'\'' == wstrText[word_end]) && 
+								(LLStringOps::isAlnum(wstrText[word_end - 1])) && (LLStringOps::isAlnum(wstrText[word_end + 1])))) )
+					{
+						word_end++;
+					}
+					if (word_end > seg_end)
+						break;
+
+					// Don't process words shorter than 3 characters
+					std::string word = wstring_to_utf8str(wstrText.substr(word_start, word_end - word_start));
+					if ( (word.length() >= 3) && (!LLSpellChecker::instance().checkSpelling(word)) )
+					{
+						mMisspellRanges.push_back(std::pair<U32, U32>(word_start, word_end));
+					}
+
+					// Find the start of the next word
+					word_start = word_end + 1;
+					while ( (word_start < seg_end) && (!LLWStringUtil::isPartOfWord(wstrText[word_start])) )
+						word_start++;
+				}
+			}
+
+			mSpellCheckStart = start;
+			mSpellCheckEnd = end;
+		}
+	}
+
 	LLTextSegmentPtr cur_segment = *seg_iter;
 
+	std::list<std::pair<U32, U32> >::const_iterator misspell_it = std::lower_bound(mMisspellRanges.begin(), mMisspellRanges.end(), std::pair<U32, U32>(line_start, 0));
 	for (S32 cur_line = first_line; cur_line < last_line; cur_line++)
 	{
 		S32 next_line = cur_line + 1;
@@ -566,7 +655,8 @@ void LLTextBase::drawText()
 				cur_segment = *seg_iter;
 			}
 			
-			S32 clipped_end	=	llmin( line_end, cur_segment->getEnd() )  - cur_segment->getStart();
+			S32 seg_end = llmin(line_end, cur_segment->getEnd());
+			S32 clipped_end	= seg_end - cur_segment->getStart();
 
 			if (mUseEllipses								// using ellipses
 				&& clipped_end == line_end					// last segment on line
@@ -578,6 +668,35 @@ void LLTextBase::drawText()
 				text_rect.mRight -= 2;
 			}
 
+			// Draw squiggly lines under any visible misspelled words
+			while ( (mMisspellRanges.end() != misspell_it) && (misspell_it->first < seg_end) && (misspell_it->second > seg_start) )
+			{
+				// Skip the current word if the user is still busy editing it
+				if ( (!mSpellCheckTimer.hasExpired()) && (misspell_it->first <= (U32)mCursorPos) && (misspell_it->second >= (U32)mCursorPos) )
+ 					continue;
+
+				S32 squiggle_start = 0, squiggle_end = 0, pony = 0;
+				cur_segment->getDimensions(seg_start - cur_segment->getStart(), misspell_it->first - seg_start, squiggle_start, pony);
+				cur_segment->getDimensions(misspell_it->first - cur_segment->getStart(), misspell_it->second - misspell_it->first, squiggle_end, pony);
+				squiggle_start += text_rect.mLeft;
+
+				pony = (squiggle_end + 3) / 6;
+				squiggle_start += squiggle_end / 2 - pony * 3;
+				squiggle_end = squiggle_start + pony * 6;
+
+				gGL.color4ub(255, 0, 0, 200);
+				while (squiggle_start < squiggle_end)
+				{
+					gl_line_2d(squiggle_start, text_rect.mBottom - 2, squiggle_start + 3, text_rect.mBottom + 1);
+					gl_line_2d(squiggle_start + 3, text_rect.mBottom + 1, squiggle_start + 6, text_rect.mBottom - 2);
+					squiggle_start += 6;
+				}
+
+				if (misspell_it->second > seg_end)
+					break;
+				++misspell_it;
+			}
+
 			text_rect.mLeft = (S32)(cur_segment->draw(seg_start - cur_segment->getStart(), clipped_end, selection_left, selection_right, text_rect));
 
 			seg_start = clipped_end + cur_segment->getStart();
@@ -1103,6 +1222,95 @@ void LLTextBase::deselect()
 	mIsSelecting = FALSE;
 }
 
+bool LLTextBase::getSpellCheck() const
+{
+	return (LLSpellChecker::getUseSpellCheck()) && (!mReadOnly) && (mSpellCheck);
+}
+
+const std::string& LLTextBase::getSuggestion(U32 index) const
+{
+	return (index < mSuggestionList.size()) ? mSuggestionList[index] : LLStringUtil::null;
+}
+
+U32 LLTextBase::getSuggestionCount() const
+{
+	return mSuggestionList.size();
+}
+
+void LLTextBase::replaceWithSuggestion(U32 index)
+{
+	for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it)
+	{
+		if ( (it->first <= (U32)mCursorPos) && (it->second >= (U32)mCursorPos) )
+		{
+			deselect();
+
+			// Delete the misspelled word
+			removeStringNoUndo(it->first, it->second - it->first);
+
+			// Insert the suggestion in its place
+			LLWString suggestion = utf8str_to_wstring(mSuggestionList[index]);
+			insertStringNoUndo(it->first, utf8str_to_wstring(mSuggestionList[index]));
+			setCursorPos(it->first + (S32)suggestion.length());
+
+			break;
+		}
+	}
+	mSpellCheckStart = mSpellCheckEnd = -1;
+}
+
+void LLTextBase::addToDictionary()
+{
+	if (canAddToDictionary())
+	{
+		LLSpellChecker::instance().addToCustomDictionary(getMisspelledWord(mCursorPos));
+	}
+}
+
+bool LLTextBase::canAddToDictionary() const
+{
+	return (getSpellCheck()) && (isMisspelledWord(mCursorPos));
+}
+
+void LLTextBase::addToIgnore()
+{
+	if (canAddToIgnore())
+	{
+		LLSpellChecker::instance().addToIgnoreList(getMisspelledWord(mCursorPos));
+	}
+}
+
+bool LLTextBase::canAddToIgnore() const
+{
+	return (getSpellCheck()) && (isMisspelledWord(mCursorPos));
+}
+
+std::string LLTextBase::getMisspelledWord(U32 pos) const
+{
+	for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it)
+	{
+		if ( (it->first <= pos) && (it->second >= pos) )
+			return wstring_to_utf8str(getWText().substr(it->first, it->second - it->first));
+	}
+	return LLStringUtil::null;
+}
+
+bool LLTextBase::isMisspelledWord(U32 pos) const
+{
+	for (std::list<std::pair<U32, U32> >::const_iterator it = mMisspellRanges.begin(); it != mMisspellRanges.end(); ++it)
+	{
+		if ( (it->first <= pos) && (it->second >= pos) )
+			return true;
+	}
+	return false;
+}
+
+void LLTextBase::onSpellCheckSettingsChange()
+{
+	// Recheck the spelling on every change
+	mMisspellRanges.clear();
+	mSpellCheckStart = mSpellCheckEnd = -1;
+}
 
 // Sets the scrollbar from the cursor position
 void LLTextBase::updateScrollFromCursor()
diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h
index 0549141b72a20531d5b10af3601bbf36eeb52bf9..90b147cee1dfc35d853a3e221dca9a46ea6d22c8 100644
--- a/indra/llui/lltextbase.h
+++ b/indra/llui/lltextbase.h
@@ -30,6 +30,7 @@
 
 #include "v4color.h"
 #include "lleditmenuhandler.h"
+#include "llspellcheckmenuhandler.h"
 #include "llstyle.h"
 #include "llkeywords.h"
 #include "llpanel.h"
@@ -230,7 +231,8 @@ typedef LLPointer<LLTextSegment> LLTextSegmentPtr;
 ///
 class LLTextBase 
 :	public LLUICtrl,
-	protected LLEditMenuHandler
+	protected LLEditMenuHandler,
+	public LLSpellCheckMenuHandler
 {
 public:
 	friend class LLTextSegment;
@@ -259,6 +261,7 @@ class LLTextBase
 								border_visible,
 								track_end,
 								read_only,
+								spellcheck,
 								allow_scroll,
 								plain_text,
 								wrap,
@@ -311,6 +314,24 @@ class LLTextBase
 	/*virtual*/ BOOL		canDeselect() const;
 	/*virtual*/ void		deselect();
 
+	// LLSpellCheckMenuHandler overrides
+	/*virtual*/ bool		getSpellCheck() const;
+
+	/*virtual*/ const std::string& getSuggestion(U32 index) const;
+	/*virtual*/ U32			getSuggestionCount() const;
+	/*virtual*/ void		replaceWithSuggestion(U32 index);
+
+	/*virtual*/ void		addToDictionary();
+	/*virtual*/ bool		canAddToDictionary() const;
+
+	/*virtual*/ void		addToIgnore();
+	/*virtual*/ bool		canAddToIgnore() const;
+
+	// Spell checking helper functions
+	std::string				getMisspelledWord(U32 pos) const;
+	bool					isMisspelledWord(U32 pos) const;
+	void					onSpellCheckSettingsChange();
+
 	// used by LLTextSegment layout code
 	bool					getWordWrap() { return mWordWrap; }
 	bool					getUseEllipses() { return mUseEllipses; }
@@ -540,6 +561,14 @@ class LLTextBase
 	
 	BOOL						mIsSelecting;		// Are we in the middle of a drag-select? 
 
+	// spell checking
+	bool						mSpellCheck;
+	S32							mSpellCheckStart;
+	S32							mSpellCheckEnd;
+	LLTimer						mSpellCheckTimer;
+	std::list<std::pair<U32, U32> > mMisspellRanges;
+	std::vector<std::string>		mSuggestionList;
+
 	// configuration
 	S32							mHPad;				// padding on left of text
 	S32							mVPad;				// padding above text
diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp
index 3a23ce1cacad667d63fce353adf560e4392e3d53..c5957838ba4d517e84a97caeaac46ecc1271c803 100644
--- a/indra/llui/lltexteditor.cpp
+++ b/indra/llui/lltexteditor.cpp
@@ -54,6 +54,7 @@
 #include "llwindow.h"
 #include "lltextparser.h"
 #include "llscrollcontainer.h"
+#include "llspellcheck.h"
 #include "llpanel.h"
 #include "llurlregistry.h"
 #include "lltooltip.h"
@@ -77,6 +78,7 @@ template class LLTextEditor* LLView::getChild<class LLTextEditor>(
 const S32	UI_TEXTEDITOR_LINE_NUMBER_MARGIN = 32;
 const S32	UI_TEXTEDITOR_LINE_NUMBER_DIGITS = 4;
 const S32	SPACES_PER_TAB = 4;
+const F32	SPELLCHECK_DELAY = 0.5f;	// delay between the last keypress and spell checking the word the cursor is on
 
 ///////////////////////////////////////////////////////////////////
 
@@ -1961,7 +1963,34 @@ void LLTextEditor::showContextMenu(S32 x, S32 y)
 
 	S32 screen_x, screen_y;
 	localPointToScreen(x, y, &screen_x, &screen_y);
-	mContextMenu->show(screen_x, screen_y);
+
+	setCursorAtLocalPos(x, y, false);
+	if (hasSelection())
+	{
+		if ( (mCursorPos < llmin(mSelectionStart, mSelectionEnd)) || (mCursorPos > llmax(mSelectionStart, mSelectionEnd)) )
+			deselect();
+		else
+			setCursorPos(llmax(mSelectionStart, mSelectionEnd));
+	}
+
+	bool use_spellcheck = getSpellCheck(), is_misspelled = false;
+	if (use_spellcheck)
+	{
+		mSuggestionList.clear();
+
+		// If the cursor is on a misspelled word, retrieve suggestions for it
+		std::string misspelled_word = getMisspelledWord(mCursorPos);
+		if ((is_misspelled = !misspelled_word.empty()) == true)
+		{
+			LLSpellChecker::instance().getSuggestions(misspelled_word, mSuggestionList);
+		}
+	}
+
+	mContextMenu->setItemVisible("Suggestion Separator", (use_spellcheck) && (!mSuggestionList.empty()));
+	mContextMenu->setItemVisible("Add to Dictionary", (use_spellcheck) && (is_misspelled));
+	mContextMenu->setItemVisible("Add to Ignore", (use_spellcheck) && (is_misspelled));
+	mContextMenu->setItemVisible("Spellcheck Separator", (use_spellcheck) && (is_misspelled));
+	mContextMenu->show(screen_x, screen_y, this);
 }
 
 
@@ -2846,6 +2875,9 @@ void LLTextEditor::setKeystrokeCallback(const keystroke_signal_t::slot_type& cal
 void LLTextEditor::onKeyStroke()
 {
 	mKeystrokeSignal(this);
+
+	mSpellCheckStart = mSpellCheckEnd = -1;
+	mSpellCheckTimer.setTimerExpirySec(SPELLCHECK_DELAY);
 }
 
 //virtual
diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp
index a333989e7e5951319f38aa05027acc803965821e..29b07d247986b2a8eca3435954d6058512d62bc3 100755
--- a/indra/newview/llfloaterpreference.cpp
+++ b/indra/newview/llfloaterpreference.cpp
@@ -66,6 +66,7 @@
 #include "llsky.h"
 #include "llscrolllistctrl.h"
 #include "llscrolllistitem.h"
+#include "llspellcheck.h"
 #include "llsliderctrl.h"
 #include "lltabcontainer.h"
 #include "lltrans.h"
@@ -110,6 +111,8 @@
 #include "lllogininstance.h"        // to check if logged in yet
 #include "llsdserialize.h"
 
+#include <boost/algorithm/string.hpp>
+
 const F32 MAX_USER_FAR_CLIP = 512.f;
 const F32 MIN_USER_FAR_CLIP = 64.f;
 const F32 BANDWIDTH_UPDATER_TIMEOUT = 0.5f;
@@ -445,6 +448,9 @@ BOOL LLFloaterPreference::postBuild()
 
 	getChild<LLComboBox>("language_combobox")->setCommitCallback(boost::bind(&LLFloaterPreference::onLanguageChange, this));
 
+	getChild<LLUICtrl>("btn_spellcheck_moveleft")->setCommitCallback(boost::bind(&LLFloaterPreference::onClickDictMove, this, "list_spellcheck_active", "list_spellcheck_available"));
+	getChild<LLUICtrl>("btn_spellcheck_moveright")->setCommitCallback(boost::bind(&LLFloaterPreference::onClickDictMove, this, "list_spellcheck_available", "list_spellcheck_active"));
+
 	// if floater is opened before login set default localized busy message
 	if (LLStartUp::getStartupState() < STATE_STARTED)
 	{
@@ -577,6 +583,19 @@ void LLFloaterPreference::apply()
 		}
 	}
 
+	if (hasChild("check_spellcheck"), TRUE)
+	{
+		LLScrollListCtrl* list_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_active");
+		std::vector<LLScrollListItem*> list_items = list_ctrl->getAllData();
+
+		std::list<std::string> list_dict;
+		list_dict.push_back(LLSpellChecker::instance().getActiveDictionary());
+		for (std::vector<LLScrollListItem*>::const_iterator item_it = list_items.begin(); item_it != list_items.end(); ++item_it)
+			list_dict.push_back((*item_it)->getColumn(0)->getValue().asString());
+
+		gSavedSettings.setString("SpellCheckDictionary", boost::join(list_dict, ","));
+	}
+
 	saveAvatarProperties();
 
 	if (mClickActionDirty)
@@ -687,6 +706,8 @@ void LLFloaterPreference::onOpen(const LLSD& key)
 	// Load (double-)click to walk/teleport settings.
 	updateClickActionControls();
 	
+	buildDictLists();
+
 	// Enabled/disabled popups, might have been changed by user actions
 	// while preferences floater was closed.
 	buildPopupLists();
@@ -865,6 +886,25 @@ void LLFloaterPreference::onNameTagOpacityChange(const LLSD& newvalue)
 	}
 }
 
+void LLFloaterPreference::onClickDictMove(const std::string& from, const std::string& to)
+{
+	LLScrollListCtrl* from_ctrl = findChild<LLScrollListCtrl>(from);
+	LLScrollListCtrl* to_ctrl = findChild<LLScrollListCtrl>(to);
+
+	LLSD row;
+	row["columns"][0]["column"] = "name";
+	row["columns"][0]["font"]["name"] = "SANSSERIF_SMALL";
+	row["columns"][0]["font"]["style"] = "NORMAL";
+
+	std::vector<LLScrollListItem*> sel_items = from_ctrl->getAllSelected();
+	for (std::vector<LLScrollListItem*>::const_iterator sel_it = sel_items.begin(); sel_it != sel_items.end(); ++sel_it)
+	{
+		row["columns"][0]["value"] = (*sel_it)->getColumn(0)->getValue();
+		to_ctrl->addElement(row);
+	}
+	from_ctrl->deleteSelectedItems();
+}
+
 void LLFloaterPreference::onClickSetCache()
 {
 	std::string cur_name(gSavedSettings.getString("CacheLocation"));
@@ -930,6 +970,61 @@ void LLFloaterPreference::refreshSkin(void* data)
 	self->getChild<LLRadioGroup>("skin_selection", true)->setValue(sSkin);
 }
 
+void LLFloaterPreference::buildDictLists()
+{
+	LLComboBox* dict_combo = findChild<LLComboBox>("combo_spellcheck_dict");
+	dict_combo->clearRows();
+
+	LLScrollListCtrl* active_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_active");
+	active_ctrl->clearRows();
+
+	LLScrollListCtrl* avail_ctrl = findChild<LLScrollListCtrl>("list_spellcheck_available");
+	avail_ctrl->clearRows();
+
+	if (LLSpellChecker::getUseSpellCheck())
+	{
+		// Populate the main dictionary combobox
+		const LLSD& dict_map = LLSpellChecker::instance().getDictionaryMap();
+		if (dict_map.size())
+		{
+			for (LLSD::array_const_iterator dict_it = dict_map.beginArray(); dict_it != dict_map.endArray(); ++dict_it)
+			{
+				const LLSD& dict = *dict_it;
+				if ( (dict["installed"].asBoolean()) && (dict.has("language")) )
+					dict_combo->add(dict["language"].asString());
+			}
+			dict_combo->selectByValue(LLSpellChecker::instance().getActiveDictionary());
+		}
+
+		LLSD row;
+		row["columns"][0]["column"] = "name";
+		row["columns"][0]["font"]["name"] = "SANSSERIF_SMALL";
+		row["columns"][0]["font"]["style"] = "NORMAL";
+
+		// Populate the active dictionary list
+		LLSpellChecker::dict_list_t active_list = LLSpellChecker::instance().getSecondaryDictionaries();
+		active_ctrl->sortByColumnIndex(0, true);
+		for (LLSpellChecker::dict_list_t::const_iterator it = active_list.begin(); it != active_list.end(); ++it)
+		{
+			row["columns"][0]["value"] = *it;
+			active_ctrl->addElement(row);
+		}
+		active_list.push_back(LLSpellChecker::instance().getActiveDictionary());
+
+		// Populate the available dictionary list
+		avail_ctrl->sortByColumnIndex(0, true);
+		for (LLSD::array_const_iterator dict_it = dict_map.beginArray(); dict_it != dict_map.endArray(); ++dict_it)
+		{
+			const LLSD& dict = *dict_it;
+			if ( (dict["installed"].asBoolean()) && (dict.has("language")) && 
+				 (active_list.end() == std::find(active_list.begin(), active_list.end(), dict["language"].asString())) )
+			{
+				row["columns"][0]["value"] = dict["language"].asString();
+				avail_ctrl->addElement(row);
+			}
+		}
+	}
+}
 
 void LLFloaterPreference::buildPopupLists()
 {
diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h
index 7ee3294478b9e6e2da634590c7a0ba76fca6eba8..cd258b5614b427bee49b48a48a85dba037a88b08 100644
--- a/indra/newview/llfloaterpreference.h
+++ b/indra/newview/llfloaterpreference.h
@@ -121,6 +121,7 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver
 
 	void setCacheLocation(const LLStringExplicit& location);
 
+	void onClickDictMove(const std::string& from, const std::string& to);
 	void onClickSetCache();
 	void onClickResetCache();
 	void onClickSkin(LLUICtrl* ctrl,const LLSD& userdata);
@@ -160,6 +161,7 @@ class LLFloaterPreference : public LLFloater, public LLAvatarPropertiesObserver
 	void applyUIColor(LLUICtrl* ctrl, const LLSD& param);
 	void getUIColor(LLUICtrl* ctrl, const LLSD& param);
 	
+	void buildDictLists();
 	void buildPopupLists();
 	static void refreshSkin(void* data);
 private:
diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml
index 8702ebde2ab5ea8aee4ef691e0b36f611ba8c503..bc0363014a3a734f97de287cb66b0290fea6312a 100644
--- a/indra/newview/skins/default/textures/textures.xml
+++ b/indra/newview/skins/default/textures/textures.xml
@@ -54,6 +54,8 @@ with the same filename but different name
 
   <texture name="Arrow_Down" file_name="widgets/Arrow_Down.png"	preload="true" />
   <texture name="Arrow_Up" file_name="widgets/Arrow_Up.png" preload="true" />
+  <texture name="Arrow_Left" file_name="widgets/Arrow_Left.png" preload="true" />
+  <texture name="Arrow_Right" file_name="widgets/Arrow_Right.png" preload="true" />
 
   <texture name="AudioMute_Off" file_name="icons/AudioMute_Off.png" preload="false" />
   <texture name="AudioMute_Over" file_name="icons/AudioMute_Over.png" preload="false" />
diff --git a/indra/newview/skins/default/textures/widgets/Arrow_Left.png b/indra/newview/skins/default/textures/widgets/Arrow_Left.png
new file mode 100644
index 0000000000000000000000000000000000000000..a424282839c8d9c71d3785ae2ba4badccf0455db
Binary files /dev/null and b/indra/newview/skins/default/textures/widgets/Arrow_Left.png differ
diff --git a/indra/newview/skins/default/textures/widgets/Arrow_Right.png b/indra/newview/skins/default/textures/widgets/Arrow_Right.png
new file mode 100644
index 0000000000000000000000000000000000000000..e32bee8f344c989aad93c36bab229a2d5f03e3c6
Binary files /dev/null and b/indra/newview/skins/default/textures/widgets/Arrow_Right.png differ
diff --git a/indra/newview/skins/default/xui/en/floater_preferences.xml b/indra/newview/skins/default/xui/en/floater_preferences.xml
index 402868bb977659a03b3afd7afc64de835cf093dc..eebc3a9cca80848dd394fcfb55bfb2b07b32f6dd 100644
--- a/indra/newview/skins/default/xui/en/floater_preferences.xml
+++ b/indra/newview/skins/default/xui/en/floater_preferences.xml
@@ -120,6 +120,12 @@
          layout="topleft"
          help_topic="preferences_advanced1_tab"
          name="advanced1" />
+        <panel
+         class="panel_preference"
+         filename="panel_preferences_spellcheck.xml"
+         label="Spell Check"
+         layout="topleft"
+         name="spell_check" />
     </tab_container>
 
 </floater>
diff --git a/indra/newview/skins/default/xui/en/floater_preview_notecard.xml b/indra/newview/skins/default/xui/en/floater_preview_notecard.xml
index be3b2d179d5155acaf9a359baac3dc9498a7a6d9..2e1c8ce6707354e69c065b7f5b4a779aadc76ea3 100644
--- a/indra/newview/skins/default/xui/en/floater_preview_notecard.xml
+++ b/indra/newview/skins/default/xui/en/floater_preview_notecard.xml
@@ -70,6 +70,7 @@
      max_length="65536"
      name="Notecard Editor"
      parse_urls="false" 
+     spellcheck="true"
      tab_group="1"
      top="46"
      width="392"
diff --git a/indra/newview/skins/default/xui/en/panel_edit_pick.xml b/indra/newview/skins/default/xui/en/panel_edit_pick.xml
index 2ec2e03e8c9323fd59e6084a5cb808d3136b1fd7..6d0be7fdec19fbbdad59c89aaec8739d67e33f52 100644
--- a/indra/newview/skins/default/xui/en/panel_edit_pick.xml
+++ b/indra/newview/skins/default/xui/en/panel_edit_pick.xml
@@ -134,6 +134,7 @@
          top_pad="2"
          max_length="1023"
          name="pick_desc"
+         spellcheck="true"
          text_color="black"
          word_wrap="true" />
         <text
diff --git a/indra/newview/skins/default/xui/en/panel_group_notices.xml b/indra/newview/skins/default/xui/en/panel_group_notices.xml
index 607e1bb2135ba621643a3df3037e316c457f45b2..6d5fb51e85892d86d1836e4a89c8681629459821 100644
--- a/indra/newview/skins/default/xui/en/panel_group_notices.xml
+++ b/indra/newview/skins/default/xui/en/panel_group_notices.xml
@@ -141,6 +141,7 @@ Maximum 200 per group daily
          max_length_bytes="63"
          name="create_subject"
          prevalidate_callback="ascii"
+         spellcheck="true"
          width="218" />
         <text
          follows="left|top"
@@ -161,6 +162,7 @@ Maximum 200 per group daily
          left_pad="3"
          max_length="511"
          name="create_message"
+         spellcheck="true"
          top_delta="0"
          width="218"
          word_wrap="true" />
@@ -309,6 +311,7 @@ Maximum 200 per group daily
          left_pad="3"
          max_length_bytes="63"
          name="view_subject"
+         spellcheck="true"
          top_delta="-1"
          visible="false"
          width="200" />
@@ -333,6 +336,7 @@ Maximum 200 per group daily
          right="-1"
          max_length="511"
          name="view_message"
+         spellcheck="true"
          top_delta="-40"
          width="313"
          word_wrap="true" />
diff --git a/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml b/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9b5d42984690d43216e2d13b1176cb359998102a
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/panel_preferences_spellcheck.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
+<panel
+ border="true"
+ follows="left|top|right|bottom"
+ height="408"
+ label="Spell Check"
+ layout="topleft"
+ left="102"
+ name="spellcheck"
+ top="1"
+ width="517">
+    <check_box
+     control_name="SpellCheck"
+     enabled="true"
+     follows="top|left"
+     height="16"
+     label="Enable spell checking"
+     layout="topleft"
+     left="30"
+     name="check_spellcheck"
+     top="30"
+     width="250"
+    />
+    <text
+     enabled_control="SpellCheck"
+     follows="top|left"
+     height="10"
+     label="Logs:"
+     layout="topleft"
+     left="55"
+     mouse_opaque="false"
+     name="text_spellcheck_dict"
+     top_pad="15"
+     type="string"
+     width="90"
+    >
+      Main dictionary :
+    </text>
+    <combo_box
+     enabled_control="SpellCheck"
+     follows="top|left"
+     height="23"
+     layout="topleft"
+     left_pad="10"
+     name="combo_spellcheck_dict"
+     top_pad="-15"
+     width="175"
+    />
+
+    <text
+     enabled_control="SpellCheck"
+     follows="top|left"
+     height="10"
+     label="Logs:"
+     layout="topleft"
+     left="55"
+     mouse_opaque="false"
+     name="text_spellcheck_additional"
+     top_pad="15"
+     type="string"
+     width="190"
+    >
+      Additional dictionaries :
+    </text>
+    <text
+     follows="top|left"
+     height="12"
+     layout="topleft"
+     left="80"
+     length="1"
+     name="text_spellcheck_available"
+     top_pad="10"
+     type="string"
+     width="175">
+        Available
+    </text>
+    <text
+     follows="top|left"
+     height="12"
+     type="string"
+     left_pad="45"
+     length="1"
+     layout="topleft"
+     name="text_spellcheck_active"
+     width="175">
+        Active
+    </text>
+    <scroll_list
+     follows="top|left"
+     height="155"
+     layout="topleft"
+     left="80"
+     multi_select="true"
+     name="list_spellcheck_available"
+     sort_column="0"
+     sort_ascending="true" 
+     width="175" />
+    <button
+     follows="top|left"
+     height="26"
+     image_overlay="Arrow_Right"
+     hover_glow_amount="0.15"
+     layout="topleft"
+     left_pad="10"
+     name="btn_spellcheck_moveright"
+     top_delta="50"
+     width="25">
+    </button>
+    <button
+     follows="top|left"
+     height="26"
+     image_overlay="Arrow_Left"
+     hover_glow_amount="0.15"
+     layout="topleft"
+     name="btn_spellcheck_moveleft"
+     top_delta="30"
+     width="25">
+    </button>
+    <scroll_list
+     follows="top|left"
+     height="155"
+     layout="topleft"
+     left_pad="10"
+     multi_select="true"
+     name="list_spellcheck_active"
+     sort_column="0"
+     sort_ascending="true" 
+     top_pad="-105"
+     width="175"
+    />
+
+</panel>