This project is a personalizable vocabulary widget for iOS and MacOS built with the Scriptable app. It shows a random word in multiple languages as a widget on your home/lock screen.
The focus is on daily passive exposure to vocabulary across several languages at once.
Read about the journey on Substack or Medium.
Home Screen (iOS) |
Lock Screen (iOS) |
macOS Desktop |
|
Direct LLM API calls with intelligent batching — Free, optimized, and modern approach using Google Gemini or OpenAI.
- 💰 Nearly Free: ~2 API calls/month with Gemini's free tier (vs ~100+ calls with other approaches)
- 🔋 Offline Resilient: 50-word cache means widget works even without internet
- ⚡ Fast & Stable: Words rotate at fixed intervals, no flickering on refresh
- 🎯 Simple Setup: Just an API key, no infrastructure needed
- 🌍 High Quality: Excellent translations across 20+ languages
- Batch Mode: Fetches 50 words in one API call, rotates through them over time (~2 calls/month vs ~100)
- Smart Rotation: Configurable word rotation interval (5 minutes for testing, 1 hour for production)
- TTL-based Deduplication: Remembers words for 24 hours to prevent repetition
- Theme Change Detection: Immediate cache clear when theme changes
- Robust Error Handling: Graceful fallbacks with user-friendly error messages
- Offline Resilient: Operates with cached words when API unavailable
- Provider Flexibility: Easy switching between Gemini (free) and OpenAI
- Pre-fetches words in batches, stores locally in
word_cache.json - Rotates to new word at configured interval, stable across widget refreshes
- Automatically refetches when cache runs low (< 10 words remaining)
- Tracks display history with timestamps for intelligent deduplication
- Clears cache immediately when
THEMEconstant changes
📖 Read detailed technical documentation for architecture, configuration, and troubleshooting.
- Languages: Edit the
LANG_CONFIGarray inscript_llm.js(same as Elastic version) - Theme: Edit the
THEMEvariable inscript_llm.js(default: "anything") - Rotation Interval: Change
WORD_ROTATION_INTERVAL(5 min testing, 60 min production) - Batch Size: Adjust
BATCH_SIZE(default: 50 words per API call) - Provider: Change
ACTIVE_PROVIDERto switch between"gemini"or"openai" - Model: Edit
PROVIDER_CONFIGto use different models
Option 1: Use the setup helper script (recommended)
- Copy
setup_keychain.jsto Scriptable - Run it once in Scriptable
- Select "Gemini API Key" (or "OpenAI API Key")
- Paste your API key from Google AI Studio
- Copy
script_llm.jsto Scriptable and add widget
Option 2: Manual Keychain setup
Run this code once inside Scriptable to store your API key:
// One-time: run inside Scriptable to store key in system Keychain
Keychain.set("GEMINI_API_KEY", "your-gemini-api-key-here");
// Or for OpenAI:
// Keychain.set("OPENAI_API_KEY", "your-openai-api-key-here");For Node.js testing:
export GEMINI_API_KEY="your-key-here"
node script_llm.js- ✅ Free tier - No usage limits, perfect for batch mode (~2 calls/month)
- ✅ Translation-optimized - Specifically designed for translation tasks
- ✅ High-quality output - Excellent translation accuracy across 20+ languages
- ✅ Latest generation - Gemini 3.1 (newest available)
- ✅ Fast response time - Optimized for quick API responses
For teams with existing Elastic infrastructure, an Elastic Workflow integration is available. This approach centralizes word generation and translation logic in your Elastic stack.
Key difference: Single API call per widget refresh (~100-720/month) vs batch mode (~2/month).
Best for:
- Organizations already using Elastic
- Teams needing centralized configuration
- Use cases requiring Elastic observability features
📖 Full Elastic Workflow documentation — Setup, configuration, and comparison guide.
Quick Start:
- Set up Elastic Agent Builder workflow (word generation + translations)
- Copy
script_elastic.jsto Scriptable - Configure via Keychain:
ELASTIC_API_URL,ELASTIC_API_KEY,ELASTIC_TOOL_ID - Add widget
If you prefer a simpler local or public-API approach, two alternatives are available in the scripts/ folder.
- Self-contained vocabulary entries defined in the script
- No internet connection required
- Perfect for curated or personal word lists
- Easy to customize and add your own words
- Copy
scripts/script_static.jsto Scriptable - Customize the
entriesarray with your vocabulary and setLANGS
- Fetches random words from a public random word API
- Translates words using a public translation API (LibreTranslate or similar)
- BUT multilingual support is limited
- Copy
scripts/script_api.jsto Scriptable - Configure the top-level constants (
USER_LANGUAGE_CODES,WORDS_TO_FETCH, etc.)
A minimal backend implementation is available in backend/ for multi-user support without a database.
- Anonymous
POST /api/wordendpoint - Provider abstraction (
geminioropenai) - Basic request validation and timeout handling
- Simple per-IP or
x-client-idin-memory rate limiting
cd backend
cp .env.example .env
pnpm install
export $(grep -v '^#' .env | xargs)
pnpm startSee docs/backend-v1.md for API contract and deployment notes.
If you prefer Bun:
cd backend
cp .env.example .env
bun install
export $(grep -v '^#' .env | xargs)
bun run startRun a deployment smoke test (health, generate, invalid-request validation):
cd backend
bun run test:deployUse a different deployment URL if needed:
cd backend
BASE_URL="https://your-service.onrender.com" bun run test:deployTo run Scriptable against your deployed Render service (instead of direct provider calls):
- Copy
script_backend.jsto Scriptable - Add widget and run
Notes:
script_backend.jskeeps the same local cache/rotation/fallback behavior asscript_llm.js- Backend request contract is minimal (
count+theme) - Deduplication remains client-side via local history cache
script_backend.jsuseshttps://multi-lingual-word-widget.onrender.comby defaultsetup_keychain.jsremains the direct-LLM setup helper (Gemini/OpenAI keys)


