Or DEVONthink’s script assistant (see Data > New > Script… if a provider is configured in Settings > AI > Chat). Maybe a little bit bloated but seems to work:
-- ============================================================================
-- Multilingual Search Query Translator for DEVONthink
-- ------------------------------------------------------------------------
-- What it does:
-- 1. Reads the query that is currently in the search field of the frontmost
-- main window (if empty, it asks you to type one).
-- 2. Sends it to DEVONthink's built-in AI with the instruction to translate
-- ONLY the search terms and to keep all operators (AND, OR, NOT, NEAR/n,
-- parentheses, quotes, wildcards, field prefixes) untouched.
-- 3. Appends the translated queries to the original one, combined with OR,
-- writes the result back into the search field and runs the search.
-- The combined query is also put on the clipboard as a backup.
--
-- Requirements: DEVONthink 4 with a configured AI engine
-- (Settings > AI > Chat).
--
-- How to trigger it:
-- - Save as script in ~/Library/Application Scripts/com.devon-technologies.think/Menu/
-- (then it appears in DEVONthink's script menu and can get a shortcut), or
-- - Keyboard Maestro: new macro, hot key trigger (e.g. control-option-command-F),
-- action "Execute an AppleScript" and paste this script into it.
--
-- Adjust the languages below to your four languages.
-- ============================================================================
property sourceLanguage : "German"
property targetLanguages : {"English", "French", "Spanish"}
property chatTemperature : 0.0
property chatModel : "" -- empty = DEVONthink's default model, e.g. "gpt-4.1"
-- ============================== main routine ===============================
tell application id "DNtp"
try
if (count of main windows) is 0 then error "Please open a DEVONthink main window first."
set theWindow to main window 1
-- current content of the search field
set originalQuery to ""
try
set originalQuery to my trimBlanks((search query of theWindow) as string)
end try
if originalQuery is "" then
set enteredQuery to display name editor "Multilingual Search" info "Enter the " & sourceLanguage & " search query (operators allowed):" default answer ""
if enteredQuery is missing value then return
set originalQuery to my trimBlanks(enteredQuery as string)
end if
if originalQuery is "" then error "No search query given."
set thePrompt to my buildPrompt(originalQuery, sourceLanguage, targetLanguages)
show progress indicator "Translating search query…"
try
if chatModel is "" then
set responseText to get chat response for message thePrompt temperature chatTemperature as "text"
else
set responseText to get chat response for message thePrompt model chatModel temperature chatTemperature as "text"
end if
on error errMsg
hide progress indicator
error "The AI request failed: " & errMsg
end try
hide progress indicator
if responseText is missing value then error "The AI returned an empty response."
if my trimBlanks(responseText as string) is "" then error "The AI returned an empty response."
-- parse the answer: one translated query per line
set fenceMarker to my codeFence()
set translatedQueries to {}
repeat with aParagraph in (paragraphs of (responseText as string))
set rawLine to my trimBlanks(aParagraph as string)
if rawLine is not "" and (rawLine does not start with fenceMarker) then
set cleanedLine to my stripDecoration(rawLine, targetLanguages)
if cleanedLine is not "" then set end of translatedQueries to cleanedLine
end if
end repeat
if (count of translatedQueries) is 0 then error "No translated queries could be extracted from the AI response."
if (count of translatedQueries) > (count of targetLanguages) then
set translatedQueries to items 1 thru (count of targetLanguages) of translatedQueries
end if
-- combine original + translations
set combinedQuery to "(" & originalQuery & ")"
repeat with aQuery in translatedQueries
set combinedQuery to combinedQuery & " OR (" & (aQuery as string) & ")"
end repeat
my setClipboardTo(combinedQuery)
set search query of theWindow to combinedQuery
show search combinedQuery
if (count of translatedQueries) < (count of targetLanguages) then
my showMessage("Multilingual Search", "Only " & ((count of translatedQueries) as string) & " of " & ((count of targetLanguages) as string) & " translations were returned. The search was started with the available ones.")
end if
on error errMsg number errNum
if errNum is not -128 then
try
hide progress indicator
end try
my showMessage("Multilingual Search failed", errMsg)
end if
end try
end tell
-- ================================ handlers =================================
on codeFence()
set backTick to character id 96
return backTick & backTick & backTick
end codeFence
on buildPrompt(theQuery, srcLang, theLanguages)
set nl to linefeed
set langCount to count of theLanguages
set langList to ""
repeat with i from 1 to langCount
set langList to langList & (item i of theLanguages as string)
if i < langCount then set langList to langList & ", "
end repeat
set promptText to "You translate search queries for the document manager DEVONthink." & nl & nl
set promptText to promptText & "Translate the following search query from " & srcLang & " into these languages, in exactly this order: " & langList & "." & nl & nl
set promptText to promptText & "Rules:" & nl
set promptText to promptText & "1. Translate ONLY the search terms (single words and quoted phrases)." & nl
set promptText to promptText & "2. Keep every operator and all syntax completely unchanged: AND, OR, NOT, NEAR/n, BEFORE/n, AFTER/n, parentheses, double quotes, wildcards (* and ?), ~ and field prefixes such as name:, text:, tag:, comment:, kind:, date:, filename:." & nl
set promptText to promptText & "3. Never translate operators, field prefixes, numbers, units or dates." & nl
set promptText to promptText & "4. Keep the structure, the order and the nesting of the original query exactly as they are." & nl
set promptText to promptText & "5. Use the wording a professional of that language would use. If a single term translates into several words, put the translation in double quotes so that it stays one phrase." & nl
set promptText to promptText & "6. Answer with exactly " & (langCount as string) & " lines: one complete translated query per line, in the order of the languages listed above." & nl
set promptText to promptText & "7. No language names, no numbering, no bullet points, no quotes around a whole line, no explanations, no code fences." & nl & nl
set promptText to promptText & "Query:" & nl & theQuery
return promptText
end buildPrompt
on stripDecoration(givenLine, theLanguages)
set resultLine to my trimBlanks(givenLine)
-- remove a leading language label like "English:" or "English -"
repeat with aLang in theLanguages
set langName to (aLang as string)
try
if resultLine starts with (langName & ":") then
set resultLine to my trimBlanks(characters ((count of langName) + 2) thru -1 of resultLine)
exit repeat
else if resultLine starts with (langName & " -") then
set resultLine to my trimBlanks(characters ((count of langName) + 3) thru -1 of resultLine)
exit repeat
end if
end try
end repeat
-- remove leading list markers or numbering
try
if (count of resultLine) > 2 then
set firstChar to (character 1 of resultLine) as string
set secondChar to (character 2 of resultLine) as string
if firstChar is in {"-", "*", "•"} then
set resultLine to my trimBlanks(characters 2 thru -1 of resultLine)
else if firstChar is in {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"} and secondChar is in {".", ")", ":"} then
set resultLine to my trimBlanks(characters 3 thru -1 of resultLine)
end if
end if
end try
-- remove quotes wrapping the complete query (would break the operators)
try
if (count of resultLine) > 2 and my quoteCount(resultLine) is 2 then
if ((character 1 of resultLine) as string) is (my quoteChar()) and ((character -1 of resultLine) as string) is (my quoteChar()) then
set innerPart to characters 2 thru -2 of resultLine
if innerPart contains "(" or innerPart contains " OR " or innerPart contains " AND " or innerPart contains " NOT " or innerPart contains "NEAR/" then
set resultLine to my trimBlanks(innerPart)
end if
end if
end if
end try
return resultLine
end stripDecoration
on quoteChar()
return character id 34
end quoteChar
on quoteCount(givenText)
set savedDelims to AppleScript's text item delimiters
set AppleScript's text item delimiters to my quoteChar()
set theCount to (count of text items of givenText) - 1
set AppleScript's text item delimiters to savedDelims
return theCount
end quoteCount
on trimBlanks(givenText)
set blankChars to {" ", tab, return, linefeed}
set trimmedText to givenText as string
repeat while trimmedText is not "" and (((character 1 of trimmedText) as string) is in blankChars)
if (count of trimmedText) is 1 then
set trimmedText to ""
else
set trimmedText to characters 2 thru -1 of trimmedText
end if
end repeat
repeat while trimmedText is not "" and (((character -1 of trimmedText) as string) is in blankChars)
if (count of trimmedText) is 1 then
set trimmedText to ""
else
set trimmedText to characters 1 thru -2 of trimmedText
end if
end repeat
return trimmedText
end trimBlanks
on setClipboardTo(givenText)
try
set the clipboard to givenText
end try
end setClipboardTo
on showMessage(theTitle, theMessage)
try
display alert theTitle message (theMessage as string)
end try
end showMessage