Shell script to toggle spelling language

I am really close to a fully working solution on this. The following script successfully toggles the spelling language between English and French in Keyboard settings, however the change isn’t effected in any application that is already open. It has to be restarted for the new spelling language setting to take effect.

    # Check the current language setting
    CURRENT_LANGUAGE="$(defaults read -g NSPreferredSpellServerLanguage)"
    echo "Current language setting: $CURRENT_LANGUAGE"

    # Toggle the language setting
    if [ "$CURRENT_LANGUAGE" = "en" ]; then
       defaults write -g NSPreferredSpellServerLanguage "fr"
       echo "Spelling language set to French"
    else
       defaults write -g NSPreferredSpellServerLanguage "en"
       echo "Spelling language set to English"
    fi

That doesn’t happen when the language is changed manually in System settings. Placing your cursor at the end of any text already typed in and app already running and pressing Enter forces macOS’s spellcheck / autocorrect to update itself automatically to the new language. Doing the same using the script above doesn’t effect the change without restarting the app, unfortunately.

Changing the language manually through the UI must trigger an additional line of command that allows the change to take effect in whatever apps already running, but I don’t know what it is. ChatGPT has tried a few things things (killall cfprefsd, killall -HUP AppleSpell, killall SystemUIServer, get the name of currently running apps through osascript to effect the change…), but nothing has worked so far.

I’m not a coder so I’m fumbling my way through this. Hoping someone with actual knowledge of shell scripting and macOS will be able to assist.

Cheers.

Check this answer in the apple forums from 2017. It has a few links that do a lot of explaining.

Short answer: if the app you are using relies on the applespell service, then the app needs be be restarted to use the alternate language.

https://discussions.apple.com/thread/1290020?answerId=6125620022#6125620022

Look in the dictionary of the app you are using and see if it allows you the set a spell language anywhere - or if it allows language by doc (word does this by text range even)

Thanks for the feedback and link. What doesn’t make sense to me (but again, I’m not a coder) is that running apps (at least TextEdit and Apple Mail in my testing) do not have to be restarted when the spelling language is changed manually or through AppleScript by scripting the UI, it seems to me that technically there is a way to effect the change in apps already running without restarting them, at least in whatever language that part of macOS is written in (Objective C or Swift?). The need to restart the application when the setting is changed through shell scripting might be due to the language used in the script? Again, I’m not a coder so I’m just using (possibly way off) layman reasoning.

The reason I’m trying to come up with shell script vs using the AppleScript that works is that the latter is a bit long to execute and makes the UI and clicks visible. I’d like to have something faster and that is done in the background. For some interested in looking at it, the AppleScript is:

on open_settings_to(settings_pane)
	if settings_pane = "Wi-Fi" then
		set settings_pane to "Wi‑Fi"
	end if
	tell application "System Settings"
		activate
	end tell
	tell application "System Events"
		tell application process "System Settings"
			repeat until window 1 exists
				delay 0
			end repeat
			tell splitter group 1 of group 1 of window 1
				tell outline 1 of scroll area 1 of group 1
					set row_names to value of static text of UI element 1 of every row
					repeat with i from 1 to (count row_names)
						if settings_pane is not "Apple ID" then
							if item i of row_names = {settings_pane} then
								log item i of row_names & i
								select row i
								exit repeat
							end if
						else
							try
								if item 1 of item i of row_names contains settings_pane then
									select row i
									exit repeat
								end if
							end try
						end if
					end repeat
				end tell
			end tell
		end tell
	end tell
end open_settings_to

on change_language()
	tell application "System Events"
		tell application process "System Settings"
			tell group 2 of splitter group 1 of group 1 of window 1
				repeat until group 3 of scroll area 1 of group 1 exists
					delay 0
				end repeat
				tell group 3 of scroll area 1 of group 1
					click button 1
				end tell
			end tell
			repeat until group 2 of splitter group 1 of group 1 of sheet 1 of window 1 exists
				delay 0
			end repeat
			tell group 2 of splitter group 1 of group 1 of sheet 1 of window 1
				repeat until group 3 of scroll area 1 exists
					delay 0
				end repeat
				tell group 3 of scroll area 1
					click pop up button 1
					tell menu 1 of pop up button 1
						if selected of menu item "U.S. English" is true then
							click menu item "Français"
						else
							click menu item "U.S. English"
						end if
					end tell
				end tell
				click button 1
			end tell
			delay 0.1
			tell application "System Settings"
				quit
			end tell
		end tell
	end tell
end change_language

on run {}
	open_settings_to("Keyboard")
	change_language()
end run

BTW, you can also test this changing of the settings with my app “Prefs Editor”, see apps.tempel.org

There, from the File menu, choose “Globals”. Then use the search field to find the NSPreferredSpellServerLanguage setting. You can then manually change the value and see the results right away in other apps. Similar to your issuing of the defaults command, but perhaps a bit more practical.

That’s odd. The spelling service should, in theory, always read the current preference value, but it appears that it only reads that when opening a window or the app, and then caches that value. The suggestion that “flush” the change (by restarting cfprefsd etc.) are of no use - the default command already automatically flushes the data and the running apps should see the new value right away - unless Apple has implemented this badly by caching the value instead of reacting to a changed value right away as they should. It’s all part of the decreasing software quality we see at Apple :frowning:

Here’s something you could try: With Prefs Editor, search just for “spell”. That’ll show you a few more spelling related settings. See if it helps if you, after changing the language code, turning off spelling entirely, then turning it on all, and between these changes switch to the app where you want it recognized. Maybe by turning it off and on again, with the app being in the foreground, it finally decides to re-read the changed spelling setting. If that’s the case, then your script only need to add some lines to de- and re-activated the app in between. Still not pretty but better than having to relaunch the app.

Of course, one can also always report this as an issue to Apple (bugreport.apple.com). You could make a case that you’d want a Shortcut to change the spelling language, and can’t get it to work. As Shortcuts are a fairly recent feature in macOS (contrary to the virtually now-unsupported AppleScript), it has a higher chance of getting a fix.

Oh, one more thing: Your script is currently altering the global settings. But once you change these settings in any app, it’ll store them in the app’s own local prefs, and will prefers those settings over the global settings. So, it might be just that you need to change the app’s settings instead of using “-g” to set the globals. Again, try this with Prefs Editor, opening the app’s prefs and looking for the “spell” related entries. (Update: I just did a test with TextEdit: IT does not create its own local copies of the settings - and if I create local settings for it, it’ll still not react to me changing them while the app is running. Damn.)

Actually, I already have a working AppleScript to do this, and because it scripts the UI, the changes are applied to running apps the same way they are when changing the language manually. The reason I’m wanting a shell script instead is 1) the AppleScript is a bit long to execute and 2. it runs like a macro makes the System Settings GUI visible. I want something faster and done in the background.

Of course, starting the application first and then changing the language after is “workflow”. but it is happens very often that I start working on a document and forget that I haven’t changed the spelling language (I’m a translator/proofreader/copy editor) and switch back and forth between English and French all day. Again, changing the language after is not a big deal when doing so manually through System Settings as the change is registered in opened already opened when done that way. Only doing so via a shell script poses that issue.

Hello.

Sorry for the delay, I just drove 3 days cross country and was on the road all day and working at night. Thank you very much for the detailed answer.

What would be the advantage of using your app (or any plist editor vs running a “defaults” command like

defaults write -g NSPreferredSpellServerLanguage "fr" ?

I knew that restarting cfprefsd didn’t work. ChatGPT (don’t judge, I’m getting help wherever I can) already suggested that, and it didn’t work.

I might try to play with your app, but after one of ChatGPT’s suggestions bricked my MBP last week (spent hours reinstalling everything), I’m not sure I want to keep messing with stuff I don’t understand.

The script is altering global settings as I use several apps at the same time that need to register the change including TextEdit, MS Word and Mail.

Of course, a workaround would be for me to remember to change the language before opening any of those apps, but I regularly remember to change the language after I’ve started working on a project. Changing the language in System Settings solves the problem, but doing so manually multiple times a day is a drag. I have an AppleScript that works but takes a bit long to execute and makes the System Settings GUI visible, so I’m trying to do so via shell script but I can’t find a solution to make the spelling service see the changed language setting.

I emailed Craig Federighi about the issue, and here is his reply:

Thanks for your note.

Unfortunately, there is currently no supported API (or Shortcut) for notifying running apps to refresh their view of the default system spelling language.

Sorry!

-Craig

That doesn’t tell me anything I didn’t already know, but it’s not to say there isn’t an unsupported way to achieve it. What I don’t understand is that changing the spelling language manually in System Settings does make the spelling service read the new language setting in apps/documents already opened, so I would think that it indicates that it can be done programmatically. But again, I’m using a layman’s logic and I don’t know what I don’t know…

I was answering when @Fredrik71 posted.

Here is my proposition:

use framework "Foundation"
use framework "AppKit"
use scripting additions

set spellChecker to current application's NSSpellChecker's sharedSpellChecker()
if spellChecker's language() as string = "fr" then
	spellChecker's setLanguage:"en"
else
	spellChecker's setLanguage:"fr"
end if

You may want to try the preference way:

use framework "Foundation"
use scripting additions

set theKey to "NSPreferredSpellServerLanguage"
set theDef to current application's NSUserDefaults's standardUserDefaults()
set globalDomain to theDef's persistentDomainForName:(current application's NSGlobalDomain)
set thePref to globalDomain's objectForKey:theKey
if thePref is in "fr" then
	globalDomain's setObject:"en" forKey:theKey
else
	globalDomain's setObject:"fr" forKey:theKey
end if

Bonne journée!

I agree. That’s why I suggested to play around changing more settings than just the one you’re focussing on, because it might be that only re-setting one of the other related preferences triggers the reload.

To find out, other than by trial-and-error, would be to look at the code of the System Prefs pane to see what it does when you change the spelling settings. That could be done with a tool like Hopper, but still requires some deeper understanding on how to reverse-engineer compiled program code. I might be able to do that but it can take hours, and I have not the time nor inclination for that currently. Perhaps you can figure out a way to find someone else to do that for you.

One other trick you could try: Use “defaults read -g” before and after changing the System Setting, and then compare the two, e.g. with BBEdit’s “Compare…” command, to see which prefs were changed. Then try setting all the same ones yourself and see if that has the desired effect. If not, then System Prefs might be additionally calling some “private” functions into internal framework code, but that would require an analysis with Hopper, then.

Good luck.

Hello @tempel ,

I certainly don’t expect anyone to take on the task if it is time consuming, but I thank you for taking the time to point me in the right direction! I won’t experiment with tools that do things I don’t understand, but I might try your app and BBEdit and see if I can me progress. thanks again.

Hi @Fredrik71 ,

Thanks for the latest input, but in all honesty all that stuff flies right over my head. I will test out your script above though!

Hi @ionah,

Thanks for taking the time to write this! I will try your suggestions!

Bonne à journée à vous !

Hi there - thanks again for the suggestions. I have been traveling and not let been able to test them out. Did your run either one if those scripts on your machine to see if they worked as intended?

Out of curiosity, does setting Spelling to ‘automatic by language’ not suffice?

I have two sentences in a textedit document, one of Spanish text, one of English. Each includes the misspelt word ‘informasión’.

If I launch the Spelling and Grammar dialogue, within the Spanish sentence, it flags the word and suggests ‘información’. It then proceeds to same word in the English sentence and suggests ‘information’.

At first I expected that this would require me to change the language of the document but I guess when they said automatic, they meant it.

In my experience, the Automatic by Language setting eventually gets confused and starts autocorrecting / spellchecking in the wrong language.

I won’t. Language settings are touchy. You know it.

I dug out an old dusty 2012 MacBook Pro I’m running Mavericks on to test out your suggestions. Unfortunately, both return an error message.

The very first one returns:

error “Can’t make language {} of «class ocid» id «data
optr0000000030DF150000600000» into type string.” number -1700 from language {}
of «class ocid» id «data optr0000000030DF150000600000» to string

and the second:

error “-[__NSCFDictionary setObiect:forkey:]: mutating method sent to immutable
obiect” number -10000

Maverick? Really?
You can’t run AppleScriptObjC scripts in Maverick like that.
And I forgot what procedure you should do.

Try the first script on your actual machine. It should work.

Well, there’s only so

Similar error message on Ventura.

Try this:

use framework "Foundation"
use framework "AppKit"
use scripting additions

set spellChecker to current application's NSSpellChecker's sharedSpellChecker()
set theLang to spellChecker's |language|() as string
if theLang = "fr" then
	spellChecker's setLanguage:"en"
else
	spellChecker's setLanguage:"fr"
end if

No error message, unfortunately it doesn’t seem to have any effect :frowning:

Thanks all the same.