2. Building the Translation Automation
Choosing a Model
The TranslateGemma model page on Ollama lists three parameter sizes: 4b, 12b, and 27b. All have a 128K context window and accept both text and image input. Three quantization variants are available for each size: q4_K_M, q8_0, and bf16.
| Model | q4_K_M | q8_0 | bf16 |
|---|---|---|---|
| 4b | 3.3 GB | 4.9 GB | 8.6 GB |
| 12b | 8.1 GB | 13 GB | 24 GB |
| 27b | 17 GB | 30 GB | 55 GB |
Quantization is a technique that approximates model weights with low-bit integers to reduce size and improve inference speed. q4_K_M denotes 4-bit quantization; "K" refers to the K-quants method, which preserves some weights at higher precision based on the importance of each block, and "M" denotes the Medium setting, which balances size and accuracy. Specifying translategemma:12b in Ollama uses the q4_K_M variant.
In testing, the 4b model showed noticeable hallucinations and sometimes failed to translate words correctly. The 27b model showed no clear quality advantage over 12b. Based on these findings, the script uses the 12b model.
Installation
TranslateGemma is a translation model installable via Ollama. To set it up, run ollama pull translategemma:12b, then confirm the model is registered with ollama list. If translategemma:12b appears in the list, the model is ready to use.
$ ollama pull translategemma:12b
$ ollama list
NAME ID SIZE MODIFIED
translategemma:12b c2f9a9ca1ec7 8.1 GB 10 days agoAccording to the TranslateGemma documentation, the translation prompt must strictly follow a prescribed format:
You are a professional {SOURCE_LANG} ({SOURCE_CODE}) to {TARGET_LANG} ({TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original {SOURCE_LANG} text while adhering to {TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
Produce only the {TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following {SOURCE_LANG} text into {TARGET_LANG}:
{TEXT}Specifically, the source language name and code are provided as {SOURCE_LANG} and {SOURCE_CODE}, the target language name and code as {TARGET_LANG} and {TARGET_CODE}, and the text to translate as {TEXT} at the end. Two blank lines must be inserted between the instruction and {TEXT}. For example, when translating from English to Japanese, the fixed template is filled with English, Japanese, and their respective language codes en and ja, with the source text such as hello, world placed at the end.
This prompt must be written exactly as specified, and manually entering it each time is tedious. Opening Open Web UI for every translation adds further friction. The solution is a shell script that calls the Ollama API directly. Alfred can invoke shell scripts via shortcuts and pass the currently selected text as an argument, allowing selected text to be translated instantly while maintaining the required prompt format.
The following sections explain each part of the script.
translate
#!/bin/bash
# ========== Configuration ==========
KEEP_ALIVE="5m" # How long to keep model loaded (e.g. "1m", "5m", "0")
MODEL="translategemma:12b"
# ====================================
# Argument parsing
LANG_OPTION=""
TEXT=""
while [[ $# -gt 0 ]]; do
case $1 in
--en-ja)
LANG_OPTION="en-ja"
shift
;;
--ja-en)
LANG_OPTION="ja-en"
shift
;;
*)
TEXT="$1"
shift
;;
esac
done
# Fall back to clipboard if no text argument
if [ -z "$TEXT" ]; then
TEXT=$(pbpaste)
fi
# Exit if text is still empty
if [ -z "$TEXT" ]; then
osascript -e 'display notification "No text to translate" with title "Translation Error"'
exit 1
fi
# Determine translation direction
if [ "$LANG_OPTION" = "en-ja" ]; then
SOURCE_LANG="English"
SOURCE_CODE="en"
TARGET_LANG="Japanese"
TARGET_CODE="ja"
elif [ "$LANG_OPTION" = "ja-en" ]; then
SOURCE_LANG="Japanese"
SOURCE_CODE="ja"
TARGET_LANG="English"
TARGET_CODE="en"
else
# Auto-detect
if echo "$TEXT" | grep -qE '[ぁ-んァ-ヶー一-龯]'; then
SOURCE_LANG="Japanese"
SOURCE_CODE="ja"
TARGET_LANG="English"
TARGET_CODE="en"
else
SOURCE_LANG="English"
SOURCE_CODE="en"
TARGET_LANG="Japanese"
TARGET_CODE="ja"
fi
fi
# Notify translation start
osascript -e "display notification \"Translating...\" with title \"${SOURCE_LANG} → ${TARGET_LANG}\""
# Build prompt
PROMPT="You are a professional ${SOURCE_LANG} (${SOURCE_CODE}) to ${TARGET_LANG} (${TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original ${SOURCE_LANG} text while adhering to ${TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
Produce only the ${TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following ${SOURCE_LANG} text into ${TARGET_LANG}:
${TEXT}"
# Write JSON to a temp file
TMPFILE=$(mktemp)
jq -n --arg prompt "$PROMPT" --arg model "$MODEL" --arg keep_alive "$KEEP_ALIVE" '{
model: $model,
prompt: $prompt,
stream: false,
keep_alive: $keep_alive
}' > "$TMPFILE"
# Run inference via Ollama API
RESULT=$(curl -s http://localhost:11434/api/generate -d @"$TMPFILE" | jq -r '.response')
# Remove temp file
rm "$TMPFILE"
# Replace "、" with ","
RESULT=${RESULT//、/,}
# Notify and exit on empty result
if [ -z "$RESULT" ] || [ "$RESULT" = "null" ]; then
osascript -e 'display notification "Translation failed" with title "Translation Error"'
exit 1
fi
# Copy to clipboard
echo -n "$RESULT" | pbcopy
# Notify completion
osascript -e "display notification \"Translation copied to clipboard\" with title \"${SOURCE_LANG} → ${TARGET_LANG}\""
# Output result (for Alfred)
echo "$RESULT"Configuration
The configuration block at the top defines the model name and the keep_alive parameter as variables.
KEEP_ALIVE="5m" # How long to keep model loaded (e.g. "1m", "5m", "0")
MODEL="translategemma:12b"keep_alive is an Ollama API parameter that specifies how long to keep the model loaded in memory after inference completes. Setting it to "0" unloads the model immediately after the response; "5m" keeps it loaded for five minutes. Since loading a model takes several seconds, keeping it in memory reduces wait time when performing multiple translations in succession.
Argument Parsing and Text Retrieval
LANG_OPTION=""
TEXT=""
while [[ $# -gt 0 ]]; do
case $1 in
--en-ja)
LANG_OPTION="en-ja"
shift
;;
--ja-en)
LANG_OPTION="ja-en"
shift
;;
*)
TEXT="$1"
shift
;;
esac
done
if [ -z "$TEXT" ]; then
TEXT=$(pbpaste)
fiThe script accepts three kinds of arguments: --en-ja, --ja-en, and the text to translate. The while [[ $# -gt 0 ]]; do ... done loop continues as long as arguments remain, consuming them one by one with shift. --en-ja and --ja-en are stored in LANG_OPTION; any other argument is treated as the translation target TEXT.
If no text argument is provided, the clipboard contents are retrieved via pbpaste, the standard macOS command that writes clipboard contents to standard output. If the clipboard is also empty, osascript displays a notification and the script exits.
Determining Translation Direction
if [ "$LANG_OPTION" = "en-ja" ]; then
...
else
# Auto-detect
if echo "$TEXT" | grep -qE '[ぁ-んァ-ヶー一-龯]'; then
SOURCE_LANG="Japanese"
...
else
SOURCE_LANG="English"
...
fi
fiIf --en-ja or --ja-en is specified, that direction is used. Otherwise, the text is tested against the regular expression [ぁ-んァ-ヶー一-龯]; if hiragana, katakana, or CJK characters are found, the direction is Japanese → English; otherwise, English → Japanese.
Building the Prompt
PROMPT="You are a professional ${SOURCE_LANG} (${SOURCE_CODE}) to ${TARGET_LANG} (${TARGET_CODE}) translator. Your goal is to accurately convey the meaning and nuances of the original ${SOURCE_LANG} text while adhering to ${TARGET_LANG} grammar, vocabulary, and cultural sensitivities.
Produce only the ${TARGET_LANG} translation, without any additional explanations or commentary. Please translate the following ${SOURCE_LANG} text into ${TARGET_LANG}:
${TEXT}"The TranslateGemma fixed prompt described in the previous section is filled with the determined language direction variables. Bash expands variables in the form ${VAR} within double-quoted strings. The prompt spans multiple lines; two blank lines are inserted immediately before ${TEXT} as required by the TranslateGemma specification.
Calling the Ollama API
TMPFILE=$(mktemp)
jq -n --arg prompt "$PROMPT" --arg model "$MODEL" --arg keep_alive "$KEEP_ALIVE" '{
model: $model,
prompt: $prompt,
stream: false,
keep_alive: $keep_alive
}' > "$TMPFILE"
RESULT=$(curl -s http://localhost:11434/api/generate -d @"$TMPFILE" | jq -r '.response')
rm "$TMPFILE"Ollama exposes an HTTP API; inference is triggered by sending a POST request in JSON format to http://localhost:11434/api/generate. Passing the prompt directly to curl is cumbersome when the text contains Japanese characters, newlines, or quotation marks that require JSON escaping. jq is used instead. jq -n --arg name value properly JSON-escapes the provided strings, so text with special characters is handled safely. The JSON is written to a temporary file created by mktemp and sent as the request body via curl's -d @filename. Specifying stream: false causes the full response to be returned at once. The translation result is stored in the .response field of the response JSON, and jq -r extracts it as a plain string. The temporary file is removed with rm after use.
Post-processing
RESULT=${RESULT//、/,}
echo -n "$RESULT" | pbcopy
osascript -e "display notification \"Translation copied to clipboard\" with title \"${SOURCE_LANG} → ${TARGET_LANG}\""
echo "$RESULT"${RESULT//、/,} is a Bash parameter expansion that replaces every 、 (Japanese comma) with , in RESULT. Since TranslateGemma does not allow fine-grained control over output style, this kind of adjustment is applied as a post-processing step. The translation result is copied to the clipboard with pbcopy, and osascript displays a completion notification. The final echo "$RESULT" outputs the result so Alfred can display it in Large Type or similar; it also causes the result to appear on standard output when the script is run directly from the command line.