Germ is the result of my impressive inability to learn German. I had (very) slowly wrapped my head around the fundamentals of the language, and wanted to start immersing myself in some authentic German content.
The problem was that my German wasn't good enough to read anything interesting, like news articles or blog posts. At the same time, I think you'll agree that life is too short to trudge through textbook passages about Ingrid asking for directions or Wolfgang checking into a hotel. I basically wanted to find content that I was keen to read in English, but then somehow be able to read it in German instead.
Enter Germ, a humble little command line tool that takes one argument: the url of an article you want to read. It then creates a variation of the article that's designed for language learning and sends it to your Kindle, so you have some educational content waiting for you when you tuck yourself into bed that night.
This very page, getting run through Germ...

...and the end result of that process.
Here's the repo, in case you'd like to check it out play around with it yourself: https://github.com/andrewerlanger/germ
Behind the scenes, Germ:
- Converts the article to clean markdown using Jina
- Sends the markdown to Gemini, which:
- Translates the content to German at your specified proficiency (B1 in my case)
- Adds a glossary of key terms to aid comprehension
- Generates a language-learning version of the article, complete German text, English translations and a glossary of any useful terms encountered along the way
- Sends the finished e-book to your Kindle
# frozen_string_literal: true
require "faraday"
module Germ
class Conductor
def self.call(url)
new(url).call
end
def initialize(url)
@url = url
end
def call
puts "🌐 fetching article content..."
content = fetch_content
puts "📝 building prompt..."
user_prompt = build_prompt(content)
puts "⚡ generating xhtml response..."
xhtml = generate_xhtml(user_prompt)
puts "📚 generating ebook..."
result = Ebook.call(xhtml, @url)
title = result[:title]
filename = result[:filename]
puts "📧 sending to kindle..."
Kindle.call(title, filename)
puts "🚮 deleting file..."
File.delete(filename)
puts "😎 done!"
end
private
def fetch_content
url = @url.start_with?("http") ? @url : "https://r.jina.ai/#{@url}"
Faraday.get(url).body
end
def build_prompt(content)
example = File.read(File.expand_path("../../example.html", __dir__))
user_prompt = File.read(File.expand_path("../../user_prompt.md", __dir__))
user_prompt = user_prompt.gsub("{{ CONTENT }}", content)
user_prompt = user_prompt.gsub("{{ EXAMPLE }}", example)
end
def generate_xhtml(user_prompt)
response = Gemini.call(user_prompt)
response.split("```xhtml", 2).last.split("```", 2).first
end
end
end
The main thrust of the logic.
In case you were wondering, my German is still pretty terrible. But it's notably less terrible than it used to be, and I mostly have Germ to thank for that 🦠♥️