●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Automating Multilingual App Review Replies with Claude API — Real Lessons from 50M Downloads
An indie developer behind 50M+ download apps shares the full implementation of Claude API-powered multilingual review reply automation — including App Store's undocumented 8-second rule, session limits, and the three traps that can get you banned.
Until three years ago, I was manually writing every reply to reviews on Beautiful HD Wallpapers every weekend. The app had already passed 2 million monthly active users, and reviews in English, Japanese, Spanish, Arabic, Indonesian, and 15 more languages arrived daily. I was spending three hours every weekend on replies — and I could feel that it wasn't a sustainable use of time.
This article documents the journey from that manual process to a Claude API-powered automated reply system running in production. I'll include the undocumented constraints I discovered through trial and error, the mistakes I made, and the architecture that eventually worked.
Why Automation Became Necessary
A 50M+ cumulative download figure with 3 million monthly active users is something to be proud of — but it creates what I'd call the "dialogue problem." The volume of user input scales faster than any individual can handle.
Law of Attraction Everyday and Relaxing Healing made this even more acute. Users of wellness and healing apps tend to write emotionally meaningful reviews. Messages like "this app changed my mornings" deserve something more than a copy-pasted response. But replying personally to 100+ reviews a day across four apps is physically impossible.
There's also the question of reply rate's effect on store algorithms. Apple doesn't publish specifics, but the community consensus — and my own experience watching review counts correlate with algorithm behavior — suggests that responding matters.
My first attempt was using ChatGPT to draft replies, then manually copying them. That worked at small scale but didn't scale. When I tried to automate with the App Store Connect API, I hit the first of several undocumented walls.
The Undocumented App Store Constraints
The App Store Connect API has a review response endpoint. On paper it looks straightforward. In practice, submitting replies in rapid succession led to replies simply not appearing — no error, HTTP 200 status, but the response never showed on the app.
After weeks of testing, I arrived at these empirical limits:
Session limit: approximately 30–40 replies before responses start silently dropping (not documented by Apple)
Minimum interval: 8 seconds between each reply submission — consecutive sends cause later ones to silently fail
Template spam detection: sending near-identical text to multiple reviews risks being flagged
None of this is in Apple's official documentation. These constraints come from community reports and my own systematic experiments. They may change.
import timeimport asynciofrom typing import Optionalclass AppStoreReplyThrottler: """ Throttles App Store Connect API reply submissions. Constraints are empirically derived, not officially documented. """ SESSION_LIMIT = 35 # Safe session maximum (with margin) MIN_INTERVAL_SECONDS = 9.0 # 8 seconds + 1 second safety margin SESSION_COOLDOWN_HOURS = 4 # Rest period between sessions def __init__(self): self.session_count = 0 self.last_reply_time: Optional[float] = None self.session_start_time: Optional[float] = None def can_reply(self) -> bool: return self.session_count < self.SESSION_LIMIT async def wait_before_reply(self): """Enforce the minimum interval between replies.""" if self.last_reply_time is not None: elapsed = time.time() - self.last_reply_time wait_time = self.MIN_INTERVAL_SECONDS - elapsed if wait_time > 0: await asyncio.sleep(wait_time) if self.session_start_time is None: self.session_start_time = time.time() def record_reply(self): self.session_count += 1 self.last_reply_time = time.time() def reset_session(self): """Call after cooldown period expires.""" self.session_count = 0 self.session_start_time = None
Google Play's review reply API has similar constraints, though their daily limits are somewhat more explicit. Both platforms appear to use heuristics that detect sudden spikes in reply volume.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master the undocumented App Store session limit (30-40 replies) and 8-second minimum interval to run automated replies without triggering penalties
✦Implement Claude API-powered reply generation across 30+ languages with per-app tone optimization and cost control using Haiku/Sonnet routing
✦Avoid the three traps indie developers fall into: ban risk from over-replying, cost explosion from wrong model selection, and detection by App Store's template spam filter
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I evaluated several APIs for multilingual reply generation. Claude's advantage came down to two things: emotional reading across languages and consistent tone maintenance without explicit per-language fine-tuning.
My apps span a wide emotional range — from visually-focused wallpaper appreciation (Beautiful HD Wallpapers) to sincere self-improvement intent (Law of Attraction Everyday). A reply that sounds clinical or generic undermines the in-app experience. Claude consistently produced replies that felt like they came from a human who understood what the app was for.
Here's the overall system architecture:
App Store Connect API / Google Play Developer API
↓ Fetch unreplied reviews
Review Fetcher (Python)
↓ Language detection + sentiment scoring
Preprocessor
↓ App-specific + locale-specific prompt construction
Claude API (claude-haiku-4-5 / claude-sonnet-4-6)
↓ Reply generation
Quality Filter (length check, similarity check, forbidden phrases)
↓
Throttled Publisher (9-second intervals)
↓ Post to App Store Connect / Google Play API
Reply posting
↓
Log + Slack notification
Model Selection and Cost Optimization
Processing 30+ languages daily made cost a central design consideration.
Starting with claude-sonnet-4-6 for everything produced higher-than-expected monthly costs. When I analyzed the review distribution, roughly 60% were short 1–2 sentence reviews. Sending "Great app!" through Sonnet is wasteful.
I now route by complexity:
from anthropic import Anthropicfrom dataclasses import dataclassfrom enum import Enumclass ReviewComplexity(Enum): SIMPLE = "simple" # 1-2 sentences, clear sentiment MODERATE = "moderate" # 3-5 sentences, multiple signals COMPLEX = "complex" # Long-form, bug reports, feature requests@dataclassclass ReviewMetrics: word_count: int has_bug_report: bool has_feature_request: bool sentiment_score: float # -1.0 (negative) to 1.0 (positive)def determine_complexity(review: str, metrics: ReviewMetrics) -> ReviewComplexity: if metrics.has_bug_report or metrics.has_feature_request: return ReviewComplexity.COMPLEX if metrics.word_count > 50 or abs(metrics.sentiment_score) < 0.3: return ReviewComplexity.MODERATE return ReviewComplexity.SIMPLEdef select_model(complexity: ReviewComplexity) -> str: return { ReviewComplexity.SIMPLE: "claude-haiku-4-5-20251001", ReviewComplexity.MODERATE: "claude-haiku-4-5-20251001", ReviewComplexity.COMPLEX: "claude-sonnet-4-6", }[complexity]def generate_reply( client: Anthropic, review_text: str, app_name: str, locale: str, complexity: ReviewComplexity) -> str: model = select_model(complexity) system_prompt = f"""You are the support representative for {app_name}.Write a natural reply in {locale} to the user's review.Guidelines:- Keep the reply to 3-4 sentences (longer replies don't get read)- Match the tone appropriate for {app_name}- Reference something specific from the review — avoid generic openers- For bug reports and feature requests: express genuine appreciation and forward momentum- Vary your language — don't reuse phrases from other replies""" response = client.messages.create( model=model, max_tokens=300, system=system_prompt, messages=[{"role": "user", "content": f"Reply to this review:\n\n{review_text}"}] ) return response.content[0].text
This routing cut monthly API costs by approximately 40%. Simple and moderate reviews account for 70–75% of volume, so Haiku handles the majority.
Beating the Template Spam Detection
Even with Claude generating replies, a pattern emerged after processing thousands of reviews. Certain opening phrases — "Thank you for your review," "We appreciate your feedback" — appeared frequently enough to potentially trigger template detection.
The fix combines in-prompt variation enforcement with post-generation similarity checking:
import hashlibfrom difflib import SequenceMatcherclass ReplyVariationManager: """ Tracks generated replies to enforce diversity. Prevents similarity-based template spam detection. """ def __init__(self, similarity_threshold: float = 0.6): self.recent_replies: list[str] = [] self.reply_hashes: set[str] = set() self.similarity_threshold = similarity_threshold self.window_size = 50 def is_too_similar(self, new_reply: str) -> bool: new_hash = hashlib.md5(new_reply.encode()).hexdigest() if new_hash in self.reply_hashes: return True for existing in self.recent_replies[-self.window_size:]: ratio = SequenceMatcher(None, new_reply, existing).ratio() if ratio > self.similarity_threshold: return True return False def add_reply(self, reply: str): self.recent_replies.append(reply) self.reply_hashes.add(hashlib.md5(reply.encode()).hexdigest()) def get_variation_instruction(self) -> str: """Returns a prompt addition listing recently used openings to avoid.""" recent = self.recent_replies[-5:] if not recent: return "" used_openings = [] for reply in recent: opening = reply[:50].split(".")[0] used_openings.append(opening) return "\n\nAvoid starting with these recently used phrases:\n" + \ "\n".join(f"- {o}" for o in used_openings)
When similarity is too high, the system re-generates with an updated prompt listing used openings to avoid. Up to 3 re-generation attempts before routing to manual review.
Per-App Tone Profiles
Four apps with distinct audiences need distinct voices. The same system prompt doesn't serve all of them:
APP_PROFILES = { "beautiful-hd-wallpapers": { "tone": "warm and visually enthusiastic", "system_extra": "Empathize with the visual experience — how the wallpaper feels each time they see their screen", "avoid_openers": ["Thank you for your", "We appreciate"], }, "law-of-attraction": { "tone": "encouraging and genuinely sincere", "system_extra": "Convey a real wish for the user's daily life to improve — not motivational-speaker energy", "avoid_openers": ["We're so glad", "That's wonderful"], }, "relaxing-healing": { "tone": "calm and understated", "system_extra": "Match the app's quiet atmosphere — no exclamation marks, peaceful pacing", "avoid_openers": ["So happy to hear", "Amazing"], }, "ukiyo-e-wallpapers": { "tone": "culturally respectful and informative", "system_extra": "Show appreciation for the user's interest in Japanese traditional art", "avoid_openers": ["Thank you for downloading"], }}
Multilingual Edge Cases
The 30+ language requirement surfaced problems I hadn't anticipated.
Arabic (RTL) was the hardest. Claude's punctuation choices in Arabic sometimes felt unnatural to native speakers. I added an explicit guideline: use Modern Standard Arabic with formal register, avoid Western punctuation habits.
Indonesian required explicit pronoun guidance. The distinction between formal Anda and informal kamu matters significantly in customer-facing communication. Without instruction, Claude sometimes mixed them.
Portuguese required split handling — Brazilian Portuguese users have different expectations from European Portuguese users, and the two variants felt noticeably different to users.
LOCALE_GUIDELINES = { "ar": "Reply in Modern Standard Arabic. Use formal register. Follow RTL punctuation conventions.", "id": "Reply in Indonesian. Always use 'Anda' (formal second person). Formal register throughout.", "ko": "Reply in Korean. Use 합쇼체 (formal polite). Warm but professional.", "zh-Hans": "Reply in Simplified Chinese. Polite and approachable — not stiff.", "zh-Hant": "Reply in Traditional Chinese (Taiwan/Hong Kong conventions).", "tr": "Reply in Turkish. Formal but warm. Use 'siz' (formal you).", "pt-BR": "Reply in Brazilian Portuguese. Friendly and natural, not stiff.", "pt-PT": "Reply in European Portuguese. Avoid Brazilian expressions.",}
Production Lessons: Three Things I Got Wrong
Mistake 1: Not verifying that replies actually posted.
HTTP 200 from the App Store Connect API doesn't mean the reply appeared. There's often a minutes-long propagation delay. My early implementation logged "success" immediately after the API call, which led to a backlog of "posted" replies that never appeared.
async def post_and_verify_reply( client: AppStoreClient, review_id: str, reply_text: str, throttler: AppStoreReplyThrottler, max_verify_attempts: int = 5) -> bool: await throttler.wait_before_reply() success = await client.post_review_reply(review_id, reply_text) throttler.record_reply() if not success: return False for _ in range(max_verify_attempts): await asyncio.sleep(30) existing = await client.get_reply(review_id) if existing and existing.strip() == reply_text.strip(): return True return False # Route to manual verification queue
Mistake 2: Trying to fully automate negative reviews.
Roughly 5–8% of reviews require human judgment: detailed bug reports with specific device information, emotionally intense 1-star reviews, and anything touching health or privacy. I tried to handle these automatically and produced some regrettable replies before adding a manual queue.
The current threshold: any 1-star review with more than 100 words, or any review containing keywords from a "sensitive topics" list, routes to manual review.
Mistake 3: No cost ceiling led to a runaway retry loop.
One day, Claude API costs spiked to 3× the normal rate. The similarity checker was flagging nearly all generated replies as too similar, triggering regeneration loops that weren't properly bounded. A per-run cost cap fixed this:
What I haven't been able to measure: whether replies lead to rating changes. App Store doesn't expose this data at the individual review level. It's the open problem I'm still thinking about.
What's Next
The next phase is using Claude API to aggregate and analyze review trends rather than just respond to them. Which features generate consistent frustration in which regions? Which UI patterns do users in one language group love that users in another find confusing?
Replies are the reactive layer. Analysis is the proactive layer that feeds back into the development roadmap. That pipeline is what I'm building now.
For any indie developer running apps at scale with a global user base: the combination of careful rate limiting, per-app tone profiles, model routing by complexity, and a manual review fallback makes this feasible without large infrastructure. The Claude API costs stay manageable; the return in user trust is real.
I hope this saves someone the weeks I spent discovering the undocumented limits the hard way.
The Broader Architecture: Orchestrating Across Four Apps
Running automation across four distinct apps with different user bases introduced coordination challenges I hadn't planned for initially.
Each app runs its own reply queue, but they share the same Claude API budget and the same App Store Connect API credentials. This created two problems: budget contention during peak hours, and the risk of hitting App Store-wide rate limits when all four queues fired simultaneously.
The solution was a centralized job scheduler with per-app priority weights:
import asyncioimport heapqfrom dataclasses import dataclass, fieldfrom datetime import datetimefrom typing import List@dataclass(order=True)class ReplyJob: priority: float scheduled_at: float = field(compare=False) app_id: str = field(compare=False) review_id: str = field(compare=False) review_text: str = field(compare=False) locale: str = field(compare=False) complexity: str = field(compare=False)class MultiAppReplyOrchestrator: """ Orchestrates reply generation across multiple apps with shared budget and API rate limit awareness. """ APP_PRIORITY_WEIGHTS = { "beautiful-hd-wallpapers": 1.0, # Highest volume "law-of-attraction": 0.9, "relaxing-healing": 0.8, "ukiyo-e-wallpapers": 0.7, } GLOBAL_SESSION_LIMIT = 80 # Across all apps per session INTER_APP_GAP_SECONDS = 12.0 # Slightly larger gap when switching apps INTRA_APP_GAP_SECONDS = 9.0 # Standard gap within same app def __init__(self, claude_client, store_client): self.queue: List[ReplyJob] = [] self.claude_client = claude_client self.store_client = store_client self.session_count = 0 self.last_app_id: str | None = None self.last_reply_time: float | None = None def enqueue(self, app_id: str, review_id: str, review_text: str, locale: str, complexity: str, age_hours: float): """ Priority = weight * recency factor. Older reviews get a recency penalty to ensure freshness. """ weight = self.APP_PRIORITY_WEIGHTS.get(app_id, 0.5) recency_factor = max(0.3, 1.0 - (age_hours / 168.0)) # Decays over 1 week priority = -(weight * recency_factor) # Negative for min-heap job = ReplyJob( priority=priority, scheduled_at=datetime.now().timestamp(), app_id=app_id, review_id=review_id, review_text=review_text, locale=locale, complexity=complexity ) heapq.heappush(self.queue, job) async def process_next(self) -> bool: """Process the highest-priority job in the queue.""" if not self.queue or self.session_count >= self.GLOBAL_SESSION_LIMIT: return False job = heapq.heappop(self.queue) # Determine gap based on whether we're switching apps if self.last_app_id and self.last_app_id != job.app_id: gap = self.INTER_APP_GAP_SECONDS else: gap = self.INTRA_APP_GAP_SECONDS if self.last_reply_time: elapsed = asyncio.get_event_loop().time() - self.last_reply_time if elapsed < gap: await asyncio.sleep(gap - elapsed) # Generate and post reply = generate_reply( self.claude_client, job.review_text, job.app_id, job.locale, job.complexity ) success = await self.store_client.post_reply(job.review_id, reply) self.session_count += 1 self.last_reply_time = asyncio.get_event_loop().time() self.last_app_id = job.app_id return success
The inter-app gap of 12 seconds (vs. 9 seconds intra-app) is conservative, but after the early experiences with silent reply drops, I've preferred safety margins over throughput.
Handling Sentiment Extremes: Very Positive and Very Negative Reviews
The two hardest categories to reply to well are 5-star emotional reviews and 1-star frustration reviews. They're hard for opposite reasons.
Very positive reviews ("This app changed my life") demand genuine warmth without sounding hollow. If Claude produces "We're so glad this has been meaningful to you!" — the exact phrasing a template would use — it undercuts the sincerity the user brought to the review.
I addressed this by including the review text in the variation context more explicitly:
def build_positive_review_prompt(review: str, app_profile: dict) -> str: """ For highly positive reviews: reference something specific. The goal is to sound like a person who actually read it. """ return f"""This user left a genuinely positive, emotional review:"{review}"Write a reply that:1. References one specific thing they mentioned (don't be generic)2. Matches the {app_profile['tone']} tone of {app_profile['name']}3. Is 2-3 sentences maximum4. Does NOT use: "We're so glad", "Thank you for sharing", "That means so much to us"5. Sounds like it was written by someone who read the review, not a support templateWrite only the reply text, nothing else."""
Very negative reviews require a different restraint. The natural pull is to apologize extensively or to explain defensively. Neither serves the user.
After testing many variations, the structure that worked best was:
Acknowledge the specific problem without minimizing it
State what's being done or what the user can try
Offer a direct contact path for further help
def build_negative_review_prompt(review: str, app_profile: dict, locale: str) -> str: return f"""This user left a critical review (1-2 stars):"{review}"Write a reply in {locale} that:1. Acknowledges their specific complaint — don't be vague2. Provides one concrete next step (e.g., update to latest version, contact support@dolice.net)3. Does NOT apologize excessively or become defensive4. Is 2-4 sentences, professional and calm5. Avoids sounding like a PR response or an automated messageThe tone should be: a real person who is taking the feedback seriously.Write only the reply text."""
The support email (support@dolice.net) appears in negative replies because it converts a public 1-star exchange into a private channel where problems can actually be resolved.
Prompt Caching for Cost Reduction at Scale
With system prompts averaging 600-800 tokens per app-locale combination, prompt caching became economically significant.
Claude's prompt caching works with inputs over 1,024 tokens. My system prompts alone didn't hit the threshold, but by prepending the per-app context documentation (app description, tone guidelines, sample good/bad replies) I was able to push past 1,024 tokens and cache the system prompt.
from anthropic import Anthropicdef generate_reply_with_cache( client: Anthropic, review_text: str, cached_system: str, # Already-formatted system prompt > 1024 tokens model: str) -> str: """ Use cache_control to cache the system prompt. Cost reduction: ~90% on cached tokens. """ response = client.messages.create( model=model, max_tokens=300, system=[ { "type": "text", "text": cached_system, "cache_control": {"type": "ephemeral"} } ], messages=[ { "role": "user", "content": f"Reply to this review:\n\n{review_text}" } ] ) # Track cache performance usage = response.usage cache_hit = hasattr(usage, 'cache_read_input_tokens') and usage.cache_read_input_tokens > 0 return response.content[0].text, cache_hit
The system prompt for Beautiful HD Wallpapers in Japanese, for instance, includes: the app's description in Japanese, 5 example good replies (with reasoning), 3 example bad replies (with explanation of why they fail), and the locale-specific tone guidelines. This pushes the system prompt to approximately 1,800 tokens.
With caching, the effective cost per reply for Haiku dropped from roughly $0.0003 to under $0.00005 — about an 85% reduction on cached tokens. At the volume I'm running, this adds up.
Logging, Monitoring, and the Metrics That Actually Matter
It took me two months to settle on metrics worth tracking. The first version logged everything — model used, tokens consumed, generation time, similarity score, whether the post succeeded. Most of this was noise.
The metrics that ended up mattering:
@dataclassclass ReplySessionMetrics: date: str app_id: str total_reviewed: int # Total reviews fetched auto_replied: int # Successfully auto-replied manual_queued: int # Sent to human review silently_failed: int # Posted but didn't appear (verified) haiku_calls: int sonnet_calls: int cache_hits: int estimated_cost_usd: float average_similarity_score: float # Health indicator — should stay low @property def automation_rate(self) -> float: if self.total_reviewed == 0: return 0.0 return self.auto_replied / self.total_reviewed @property def cost_per_reply(self) -> float: if self.auto_replied == 0: return 0.0 return self.estimated_cost_usd / self.auto_replied
The average_similarity_score is the health metric I watch most closely. When it creeps above 0.45 (out of 1.0), it usually means the variation manager needs its window size adjusted or the system prompt needs new example variations added. It's an early warning signal before template detection becomes a risk.
silently_failed tells me when App Store's undocumented limits have tightened. If this number spikes, I reduce the session limit or increase the interval.
Running This as a Solo Developer
The total infrastructure cost for this system, running across four apps:
Claude API: approximately ¥6,000–¥8,000 per month (~$40–55)
A small Python process on a VPS: ¥500/month
Total: under ¥9,000/month (~$60)
The time I've reclaimed — previously 10–12 hours per month spent on manual replies across all four apps — now goes into feature development and the next round of updates.
I started building individual apps in 2013, and I've maintained the iOS and Android versions of these four apps through countless platform changes. The cumulative 50M+ download figure reflects years of iteration, not a single viral moment. Running a portfolio of apps at this scale solo is only possible because each piece of operational work eventually gets automated.
Review replies were one of the last remaining manual tasks in the loop. Getting this right took longer than I expected, mostly because of undocumented constraints that only appeared at scale. I hope documenting them here saves someone else the same months of trial and error.
If you're building something similar, feel free to adapt the patterns here to your own context. The throttler values, similarity thresholds, and model routing cutoffs are all starting points — your specific app mix and review volume will suggest adjustments.
Working across iOS App Store and Google Play simultaneously surfaces subtle differences worth knowing.
Rating update tracking: Google Play allows you to see when a user updates their review after you've replied. The App Store doesn't expose this. If you want to measure whether your replies lead to improved ratings, Google Play gives you at least some signal; App Store is a black box.
Reply editing: On Google Play, you can update your reply after posting. On the App Store, once a reply is posted, it's permanent. This makes quality filtering on the App Store side more critical — there's no fallback.
Review visibility timing: On the App Store, a new review can take 24-48 hours to become visible to other users after submission. Replies don't affect this timeline. Knowing this matters for priority calculation — a review that's already 36 hours old has likely been seen by potential users, making a timely reply more valuable than for a brand-new review.
Locale inference: App Store reviews include the storefront locale (US, JP, UK) but not the actual language the review is written in. A review on the US storefront might be in Spanish if it's from a US-based Spanish speaker. Language detection needs to happen on the review text itself, not from the storefront locale. I use langdetect as the first pass, with Claude as fallback for ambiguous cases shorter than 20 words.
from langdetect import detect, LangDetectExceptionimport anthropicdef detect_review_language(review_text: str, client: anthropic.Anthropic) -> str: """ Primary: langdetect library (fast, free). Fallback: Claude for short or ambiguous text. """ if len(review_text.split()) < 5: # Too short for langdetect to be reliable return detect_language_with_claude(review_text, client) try: detected = detect(review_text) # langdetect uses ISO 639-1; map to App Store locale codes as needed locale_map = {"zh-cn": "zh-Hans", "zh-tw": "zh-Hant", "pt": "pt-BR"} return locale_map.get(detected, detected) except LangDetectException: return detect_language_with_claude(review_text, client)def detect_language_with_claude(text: str, client: anthropic.Anthropic) -> str: """Use Claude for ambiguous language detection.""" response = client.messages.create( model="claude-haiku-4-5-20251001", max_tokens=10, messages=[{ "role": "user", "content": f"What language is this text written in? Reply with only the ISO 639-1 code.\n\n{text}" }] ) return response.content[0].text.strip().lower()
This two-tier detection runs quickly and accurately handles the edge cases that caused incorrect language replies in my early versions.
Before You Build This: Questions to Ask First
After running this system for months, I'd ask any developer considering a similar project:
What percentage of your reviews actually need a personal response? For a productivity tool or professional app, users may have higher expectations and require more tailored replies. For a wallpaper app, a warm but slightly more formulaic reply may be entirely appropriate. The right automation threshold varies.
Do you have a clear manual review escalation path? The 7-8% of reviews that need human attention are the most important ones — they're the users who are frustrated enough or engaged enough to write at length. If your manual queue isn't cleared regularly, the system's value drops sharply.
What's the recovery path when App Store rate limits tighten? My session limits are tuned conservatively, but Apple's behavior has changed before without notice. Build the system so that "pause all processing and notify me" is a one-command operation, not a code change.
The core value of this system isn't speed or scale — it's consistency. Every review gets a considered response, in the user's language, at a tone that matches the app they downloaded. That consistency is what builds the kind of user trust that actually improves long-term ratings.
The code here is a starting point. Your specific mix of apps, volume, and user base will suggest adjustments. Start with conservative rate limits, measure the failure rate on posting verification, and expand from there.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.