npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-polyglot-ai

v1.0.2

Published

Dynamic AI text translation for React Native and Expo apps featuring background LLM execution, built-in FlatList batch translation, and persistent local caching.

Readme

expo-polyglot-ai


Most apps speak one language and silently lose everyone else. expo-polyglot-ai changes that — drop it in, point it at any LLM, and your Expo app speaks 200+ languages automatically. Polyglot (n.) — a person, or in this case an app, fluent in many languages. Stop asking your users to meet you in English. Let them open your app and feel at home — in Spnaish, French, Arabic, Hindi, or whichever of the 200+ supported languages they call their own. No dictionary maintenance. No heavy lifting, just automated translating.

Just your existing app strings, your LLM of choice, and persistent local caching that means each string is only ever translated once. expo-polyglot-ai — because an app your users can read is an app they'll actually keep.

Features

  • 🔄 Drop-in <TranslateText> component — swap <Text> for <TranslateText> and any string translates automatically. No keys, no string IDs, just your actual text.
  • 💾 Permanent per-language cache — every translated string is stored in AsyncStorage. Each unique string is only ever sent to your API once. After that, it's instant — even offline.
  • 🌍 Zero-config language detection — reads the device locale via expo-localization on first launch. If the user's phone is in French, your app is in French. No prompt, no onboarding step.
  • 🔌 LLM-agnostic backend — point it at Claude, OpenAI, Gemini, or any custom endpoint. You own the model, the cost, and the prompt. The package just handles the wiring.
  • 🗂️ Static bundle support — pre-load auto generated translations via the bundles prop for your all app strings. Bundle hits are checked first, before cache, before any network call.
  • 🧠 Smart Flatlist batch engine with flushBatch — every translate() call within a 50ms window is grouped into one API request. Ten components mounting at once with dynamic data? One network call, not ten.
  • 🔁 currentLanguage & changeLanguage — read and set the active language anywhere in your app. Switching language flushes the pending queue instantly so no stale translations bleed through.
  • cacheReady flag — know exactly when the AsyncStorage cache has loaded for the current language. Translations wait for it automatically so nothing resolves against an empty cache.

Installation

npm install expo-polyglot-ai
npx expo install expo-localization @react-native-async-storage/async-storage
npx expo install @react-native-picker/picker  'This provides an easy way to create drop down menu list'

Setup

Wrap your app with TranslationProvider, pointing apiUrl at your translation backend:

import { TranslationProvider } from 'expo-polyglot-ai';

export default function App() {

  // The developer defines what language translations got generated in their specific project
    const MY_APP_BUNDLES = {
      fr: require("../../locales/fr.json"), //French translation
      es: require("../../locales/es.json"), //Spanish translation
      yo: require("../../locales/yo.json"), //Yoruba translation
      zu: require("../../locales/zu.json"), //IsiZulu translation
      //de: require("../../locales/de.json"), // They can add German!
      //it: require("../../locales/it.json"), // They can add Italian!
    };


  return (
    <TranslationProvider
      apiUrl="https://..." //This is the url of your cloud function that hosts your Translation LLM Agent.
      bundles={MY_APP_BUNDLES}
      apiKey={"5Sl-..."} // Developers pass their own runtime API key safely via configuration or props!
    >
      <YourApp />
    </TranslationProvider>
  );
}

Usage

Translate a single string

import { TranslateText } from 'expo-polyglot-ai';

<TranslateText style={styles.title}>Welcome back!</TranslateText>

Switch languages

import { Picker } from "@react-native-picker/picker";
import {
  FlatList,
  SafeAreaView,
  StatusBar,
  StyleSheet,
  View,
} from "react-native";
// Import your custom package components cleanly
import {
  TranslateText,
  TranslationProvider,
  useTranslation,
} from "../../polyglot/src/index.js";

// Mock list data with various string lengths to test layout text expansion
const SAMPLE_MOCK_DATA = [
  { id: "1", phrase: "Welcome back to your security dashboard profile." },
  {
    id: "2",
    phrase: "Please tap application settings to modify your configurations.",
  },
  {
    id: "3",
    phrase: "Are you sure you want to confirm your active logout request?",
  },
  { id: "4", phrase: "View profile notification alerts." },
];

// The developer defines what they built in their specific project
const MY_APP_BUNDLES = {
  fr: require("../../locales/fr.json"),
  es: require("../../locales/es.json"),
  yo: require("../../locales/yo.json"),
  zu: require("../../locales/zu.json"),
  //de: require("../../locales/de.json"), // They can add German!
  //it: require("../../locales/it.json"), // They can add Italian!
};

function MainAppScreen() {
  const { currentLanguage, changeLanguage } = useTranslation();

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />

      {/* Dynamic Translated Static Heading */}
      {/* You can style the text in NativeWind e.g className */}
      <TranslateText style={styles.heading} autoShrink>Choose Language</TranslateText>

      {/*Nativewind styling */}
      <TranslateText 
          autoShrink 
          className="text-lg font-bold text-slate-900 dark:text-white tracking-tight"
        >
          Choose Language
        </TranslateText>

      {/* 1. Language Dropdown Selector */}
      <View style={styles.pickerContainer}>
        <Picker
          selectedValue={currentLanguage}
          onValueChange={(langValue) => changeLanguage(langValue)}
          style={styles.picker}
        >
          <Picker.Item label="English (Base)" value="en" />
          <Picker.Item label="French (Français)" value="fr" />
          <Picker.Item label="Spanish (Español)" value="es" />
          <Picker.Item label="Yoruba (Èdè Yorùbá)" value="yo" />
          <Picker.Item label="Zulu (isiZulu)" value="zu" />
        </Picker>
      </View>

      <TranslateText style={styles.subHeading}>
        Dynamic Feed Content apears here
      </TranslateText>

      {/* 2. FlatList implementation with automatic caching layer protection */}
      <FlatList
        data={SAMPLE_MOCK_DATA}
        keyExtractor={(item) => item.id}
        contentContainerStyle={styles.listContainer}
        renderItem={({ item }) => (
          <View style={styles.card}>
            {/* autoShrink is enabled here! If French/Yoruba text gets 
              too long, it safely shrinks the font to keep layouts unbroken.
            */}
            <TranslateText style={styles.itemText}>Header for:</TranslateText>
            <TranslateText autoShrink style={styles.itemText}>
              {item.phrase}
            </TranslateText>
          </View>
        )}
      />
    </SafeAreaView>
  );
}

export default function App() {
  return (
    /* 3. Wrap your root with the Provider. 
      Replace this URL string with your deployed Google Cloud Function Webhook endpoint!
    */
    <TranslationProvider
      apiUrl="https://us-central1-..."
      bundles={MY_APP_BUNDLES}
      apiKey={"5Sl-..."} // Developers pass their own runtime API key safely via configuration or props!
    >
      <MainAppScreen />
    </TranslationProvider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingHorizontal: 20,
    backgroundColor: "#f8f9fa",
  },
  heading: {
    fontSize: 26,
    fontWeight: "bold",
    marginTop: 20,
    marginBottom: 10,
    color: "#1a1a1a",
  },
  subHeading: {
    fontSize: 18,
    fontWeight: "600",
    marginTop: 15,
    marginBottom: 10,
    color: "#555",
  },
  pickerContainer: {
    backgroundColor: "#ffffff",
    borderRadius: 10,
    borderWidth: 1,
    borderColor: "#e0e0e0",
    marginBottom: 10,
    overflow: "hidden",
  },
  picker: {
    width: "100%",
    height: 50,
  },
  listContainer: {
    paddingBottom: 20,
  },
  card: {
    padding: 18,
    backgroundColor: "#ffffff",
    borderRadius: 12,
    marginVertical: 8,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05,
    shadowRadius: 4,
    elevation: 2,
  },
  itemText: {
    fontSize: 16,
    color: "#333333",
    lineHeight: 22,
  },
});

Backend API contract

Your apiUrl endpoint should accept POST requests:

Single text:

{ "text": "Hello", "target_lang": "fr" }

Response:

{ "translated_text": "Bonjour" }

Batch:

{ "texts": ["Hello", "Goodbye"], "target_lang": "fr" }

Response:

{ "translated_texts": ["Bonjour", "Au revoir"] }

The backend is yours to build — point it at Claude, OpenAI, Gemini, or any LLM/translation API.

Langgraph LLM AI Translation Agent using Gemini

import functions_framework
import os
from typing import TypedDict, List
import json
from google import genai
from langgraph.graph import StateGraph, END

# === Single-string agent (unchanged) ===
class AgentState(TypedDict):
    text: str
    target_lang: str
    translation: str
    quality_check: bool
    iterations: int

client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

def translation_node(state: AgentState) -> dict:
    prompt = f"""
    You are an expert mobile UI string localization assistant. 
    Translate the following text into natural, fluent, context-aware {state['target_lang']}.
    Preserve technical variables and UI intent cleanly.
    Return ONLY the raw translated string.

    Text: "{state['text']}"
    """
    response = client.models.generate_content(model='gemini-2.5-flash', contents=prompt)
    return {
        "translation": response.text.strip(),
        "iterations": state.get('iterations', 0) + 1
    }

def validation_node(state: AgentState) -> dict:
    if state.get('iterations', 0) >= 2:
        return {"quality_check": True}

    validation_prompt = f"""
    Analyze this translated mobile application UI string. 
    Did the translation introduce unnecessary conversational phrasing, markdown, or text quotes? 
    Answer with exactly 'YES' if it is clean or 'NO' if it needs a rewrite.

    Translation: "{state['translation']}"
    """
    response = client.models.generate_content(model='gemini-2.5-flash', contents=validation_prompt)
    is_clean = "YES" in response.text.upper()
    return {"quality_check": is_clean}

def should_continue(state: AgentState):
    if state["quality_check"]:
        return END
    return "translator"

workflow = StateGraph(AgentState)
workflow.add_node("translator", translation_node)
workflow.add_node("validator", validation_node)
workflow.set_entry_point("translator")
workflow.add_edge("translator", "validator")
workflow.add_conditional_edges("validator", should_continue)
agent = workflow.compile()


# === Batch translation (new) ===
def translate_batch(texts: List[str], target_lang: str) -> List[str]:
    """
    Translates a list of strings in a single Gemini call.
    Uses a numbered-list format so the model returns strings in order,
    and we can reliably split the response back into a list.
    """
    numbered_input = "\n".join(f"{i+1}. {t}" for i, t in enumerate(texts))

    prompt = f"""
You are an expert mobile UI string localization assistant.
Translate each of the following numbered UI strings into natural, fluent,
context-aware {target_lang}. Preserve technical variables and UI intent.

Return ONLY a JSON array of strings, in the exact same order as the input,
with no markdown, no code fences, no extra commentary. The array must have
exactly {len(texts)} elements.

Input strings:
{numbered_input}
"""

    response = client.models.generate_content(model='gemini-2.5-flash', contents=prompt)
    raw = response.text.strip()

    # Strip markdown code fences if the model added them anyway
    if raw.startswith("```"):
        raw = raw.split("\n", 1)[1] if "\n" in raw else raw
        if raw.endswith("```"):
            raw = raw[:-3]
        raw = raw.strip()
        if raw.startswith("json"):
            raw = raw[4:].strip()

    try:
        translated = json.loads(raw)
        if not isinstance(translated, list):
            raise ValueError("Response is not a JSON array")
    except (json.JSONDecodeError, ValueError):
        # Fallback: if parsing fails, return originals untouched
        return texts

    # Pad/truncate to match input length defensively
    if len(translated) < len(texts):
        translated = translated + texts[len(translated):]
    elif len(translated) > len(texts):
        translated = translated[:len(texts)]

    return [str(t).strip() for t in translated]


@functions_framework.http
def polyglot_final(request):
    if request.method == 'OPTIONS':
        headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'POST',
            'Access-Control-Allow-Headers': 'Content-Type,X-API-Key',
            'Access-Control-Max-Age': '3600'
        }
        return ('', 204, headers)

    headers = {'Access-Control-Allow-Origin': '*'}

    provided_key = request.headers.get('X-API-Key')
    expected_key = os.getenv("TRANSLATION_API_KEY")

    if not expected_key or provided_key != expected_key:
        return (json.dumps({"error": "Unauthorized"}), 401, headers)

    try:
        request_json = request.get_json(silent=True)
        target_lang = request_json.get('target_lang')

        if not target_lang:
            return (json.dumps({"error": "Missing target_lang parameter"}), 400, headers)

        # === Batch mode: { "texts": [...], "target_lang": "..." } ===
        texts = request_json.get('texts')
        if texts is not None:
            if not isinstance(texts, list) or not all(isinstance(t, str) for t in texts):
                return (json.dumps({"error": "'texts' must be an array of strings"}), 400, headers)

            if len(texts) == 0:
                return (json.dumps({"translated_texts": []}), 200, headers)

            translated_texts = translate_batch(texts, target_lang)
            return (json.dumps({"translated_texts": translated_texts}), 200, headers)

        # === Single mode (existing behavior, unchanged) ===
        text = request_json.get('text')
        if not text:
            return (json.dumps({"error": "Missing input text or target_lang parameters"}), 400, headers)

        initial_input = {"text": text, "target_lang": target_lang, "iterations": 0}
        final_state = agent.invoke(initial_input)

        response_payload = {"translated_text": final_state["translation"]}
        return (json.dumps(response_payload), 200, headers)

    except Exception as e:
        return (json.dumps({"error": str(e)}), 500, headers)

Requirement Txt document

functions-framework==3.* google-genai>=1.2.0 langgraph>=0.0.10

gcloud function to deploy the llm-translation agent

cd into where main.py and requirements.txt are and run these commands in the Google Cloud SDK Shell:

gcloud functions deploy polyglot-ai-agent --gen2 --runtime python311 --trigger-http --allow-unauthenticated --region=us-central1 --entry-point polyglot_endpoint --project=your project id --set-env-vars GEMINI_API_KEY="Your Gemini API-KEY",TRANSLATION_API_KEY="Your X-API_KEY"

To give you clearance and authority to use the deployed agent:

gcloud beta run services add-iam-policy-binding polyglot-endpoint --region=us-central1 --member="allUsers" --role="roles/run.invoker"

Add this script in your Package.json to run the command "npm run translate", add extract-strings.js and translate-bundle.js files below before you run the command.


  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "web": "expo start --web",
    "lint": "expo lint",
    "translate": "node src/build/extract-strings.js src/app strings.json && node src/build/translate-bundle.js strings.json src/locales Your XPI-Key fr,es,yo,zu"
  },
  "private": true

Create build folder and paste these in:

extract-strings.js


const fs = require("fs");
const path = require("path");

const SOURCE_DIR = process.argv[2] || "./src";
const OUTPUT_FILE = process.argv[3] || "strings.json";

const VALID_EXTENSIONS = [".js", ".jsx", ".ts", ".tsx"];

// Matches: <TranslateText ...>SOME TEXT</TranslateText>
// Captures only simple string children (no JSX expressions/interpolation)
const TRANSLATE_TEXT_REGEX =
  /<TranslateText\b[^>]*>([^<{][\s\S]*?)<\/TranslateText>/g;

function walk(dir, files = []) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      walk(fullPath, files);
    } else if (VALID_EXTENSIONS.includes(path.extname(entry.name))) {
      files.push(fullPath);
    }
  }
  return files;
}

function extractFromFile(filePath, strings) {
  const content = fs.readFileSync(filePath, "utf8");
  let match;
  while ((match = TRANSLATE_TEXT_REGEX.exec(content)) !== null) {
    let text = match[1];

    // Collapse whitespace/newlines, trim
    text = text.replace(/\s+/g, " ").trim();

    // Skip empty or expression-only content (e.g. {variable})
    if (!text || text.startsWith("{")) continue;

    if (!strings[text]) {
      strings[text] = { sources: [] };
    }
    const relPath = path.relative(process.cwd(), filePath);
    if (!strings[text].sources.includes(relPath)) {
      strings[text].sources.push(relPath);
    }
  }
}

function main() {
  if (!fs.existsSync(SOURCE_DIR)) {
    console.error(`Source directory not found: ${SOURCE_DIR}`);
    process.exit(1);
  }

  const files = walk(SOURCE_DIR);
  console.log(`Scanning ${files.length} files in ${SOURCE_DIR}...`);

  const strings = {};
  for (const file of files) {
    extractFromFile(file, strings);
  }

  const uniqueStrings = Object.keys(strings);
  console.log(`Found ${uniqueStrings.length} unique strings.`);

  // Output format: array of { text, sources }
  const output = uniqueStrings.map((text) => ({
    text,
    sources: strings[text].sources,
  }));

  fs.writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2));
  console.log(`Written to ${OUTPUT_FILE}`);
}

main();

translate-bundle.jsx

const fs = require("fs");
const path = require("path");

const API_URL =
  "paste your deployed ai agent url here";

const STRINGS_FILE = process.argv[2] || "strings.json";
const OUTPUT_DIR = process.argv[3] || "./locales";
const API_KEY = process.argv[4];
const LANGUAGES = (process.argv[5] || "fr,es,yo,zu,de,it")
  .split(",")
  .map((l) => l.trim())
  .filter(Boolean);

// Delay between requests to avoid rate-limiting / quota issues (ms)
const REQUEST_DELAY_MS = 1200;

// Retry settings
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 3000;

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function translateOne(text, targetLang) {
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    try {
      const response = await fetch(API_URL, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-API-Key": API_KEY,
        },
        body: JSON.stringify({ text, target_lang: targetLang }),
      });

      if (!response.ok) {
        const errBody = await response.text();
        throw new Error(`HTTP ${response.status}: ${errBody}`);
      }

      const data = await response.json();
      if (!data.translated_text) {
        throw new Error("No translated_text in response");
      }
      return data.translated_text;
    } catch (err) {
      console.warn(
        `  Attempt ${attempt}/${MAX_RETRIES} failed for "${text.slice(0, 40)}..." -> ${targetLang}: ${err.message}`,
      );
      if (attempt < MAX_RETRIES) {
        await sleep(RETRY_DELAY_MS);
      } else {
        throw err;
      }
    }
  }
}

async function main() {
  if (!API_KEY) {
    console.error(
      "Missing API key. Usage: node translate-bundle.js <strings.json> <output-dir> <api-key> [languages]",
    );
    process.exit(1);
  }

  if (!fs.existsSync(STRINGS_FILE)) {
    console.error(`Strings file not found: ${STRINGS_FILE}`);
    process.exit(1);
  }

  const strings = JSON.parse(fs.readFileSync(STRINGS_FILE, "utf8"));
  const texts = strings.map((s) => s.text);

  if (!fs.existsSync(OUTPUT_DIR)) {
    fs.mkdirSync(OUTPUT_DIR, { recursive: true });
  }

  console.log(
    `Translating ${texts.length} strings into ${LANGUAGES.length} languages (${LANGUAGES.join(", ")})...`,
  );
  console.log(
    `Estimated time: ~${Math.ceil((texts.length * LANGUAGES.length * REQUEST_DELAY_MS) / 1000 / 60)} minutes\n`,
  );

  for (const lang of LANGUAGES) {
    const outputFile = path.join(OUTPUT_DIR, `${lang}.json`);

    // Load existing translations to resume
    let bundle = {};
    if (fs.existsSync(outputFile)) {
      bundle = JSON.parse(fs.readFileSync(outputFile, "utf8"));
      console.log(
        `[${lang}] Resuming — ${Object.keys(bundle).length} already translated.`,
      );
    } else {
      console.log(`[${lang}] Starting fresh.`);
    }

    let translatedCount = 0;
    let skippedCount = 0;

    for (let i = 0; i < texts.length; i++) {
      const text = texts[i];

      if (bundle[text]) {
        skippedCount++;
        continue;
      }

      try {
        const translated = await translateOne(text, lang);
        bundle[text] = translated;
        translatedCount++;

        console.log(
          `[${lang}] (${i + 1}/${texts.length}) "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}" -> "${translated.slice(0, 50)}${translated.length > 50 ? "..." : ""}"`,
        );

        // Save progress after every translation (resumable)
        fs.writeFileSync(outputFile, JSON.stringify(bundle, null, 2));

        await sleep(REQUEST_DELAY_MS);
      } catch (err) {
        console.error(
          `[${lang}] FAILED for "${text.slice(0, 50)}...": ${err.message}`,
        );
        console.error(
          `[${lang}] Skipping this string for now — re-run script later to retry.`,
        );
      }
    }

    console.log(
      `[${lang}] Done. Translated ${translatedCount} new, skipped ${skippedCount} cached. Total: ${Object.keys(bundle).length}\n`,
    );
  }

  console.log("All languages processed. Bundles written to:", OUTPUT_DIR);
}

main();

npm run translate

This is for automatically crawling your app and getting all strings in xxx tag and translating them for you to the languages you want. Please run the the "npm run translate" as soon as your configurations are done.

Ensure this const LANGUAGES = (process.argv[5] || "fr,es,yo,zu,de,it") in translte bundle has the same language values in package.json script "translate": "node src/build/extract-strings.js src/app strings.json && node src/build/translate-bundle.js strings.json src/locales Your XPI-Key fr,es,yo,zu".

How the translate works

const fs = require("fs");
const path = require("path");

const API_URL =
  "API_KEY = Your X-API-KEY";

const STRINGS_FILE = process.argv[2] || "strings.json";
const OUTPUT_DIR   = process.argv[3] || "./locales";
const API_KEY      = process.argv[4];
const LANGUAGES    = (process.argv[5] || "fr,es,yo,zu")
  .split(",")
  .map((l) => l.trim())
  .filter(Boolean);

// Delay between requests to avoid rate-limiting / quota issues (ms)
const REQUEST_DELAY_MS = 1200;

// Retry settings

The arguments — process.argv explained process.argv is how Node.js reads what you type after node script.js in the terminal. The positions are fixed: | Position | process.argv[n] | What it is | Default if omitted | |---|---|---|---| | 0 | process.argv[0] | Path to Node itself | Always there | | 1 | process.argv[1] | Path to the script | Always there | | 2 | process.argv[2] | Your Strings file | "strings.json" | | 3 | process.argv[3] | Output folder | "./locales" | | 4 | process.argv[4] | Your X-API Key | none — required | | 5 | process.argv[5] | Languages to translate | "fr,es,yo,zu" |//examples

So when npm run translate runs, it's calling something like:

src/build/translate-bundle.js strings.json ./locales YOUR_API_KEY fr,es,yo,zu

What each argument does

STRINGS_FILE — argv[2]

The JSON file that extract-strings.js produced in the step before this one. It contains every unique string found in your source code — things like "Welcome back", "Save Changes", "Add to Cart". This is the input.

OUTPUT_DIR — argv[3]

The folder where translated files land. After running, you'll get something like:
locales/
  fr.json
  es.json
  yo.json
  zu.json

Each file is a key-value map of { "original string": "translated string" }.

API_KEY — argv[4]

Your secret key sent as X-API-Key in the request header to your Cloud Function (polyglot-agent). This is the same apiKey prop you pass to in your app. No default — if you omit it the Cloud Function will reject the request.

LANGUAGES — argv[5]

A comma-separated list of language codes to translate into. The default "fr,es,yo,zu" means French, Spanish, Yoruba, and Zulu. You can pass as many as you need: bashnode translate-bundle.js strings.json ./locales YOUR_KEY fr,es,yo,zu,ar,hi,sw REQUEST_DELAY_MS — hardcoded at 1200

Not an argument — it's baked in. It adds a 1.2 second pause between each language request so you don't hammer your Cloud Function and hit Gemini's rate limit (which is exactly the 429 RESOURCE_EXHAUSTED error visible in your terminal at the bottom of the screenshot).

-N/B

Please follow ISO 639-1 — the two-letter language code standard — which is also what expo-localization returns from getLocales()[0]?.languageCode. That's the standard your developers need to follow. The Wikipedia link for ISO 639-1:

https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes

Props

TranslationProvider

| Prop | Type | Default | Description | |---|---|---|---| | apiUrl | string | required | Your translation backend endpoint | | bundles | key-value pairs | required | The developer defines what they built in their specific project example const MY_APP_BUNDLES = { fr: require("../../locales/fr.json")} | | apiKey | string | required | Developers pass their own runtime API key safely via configuration or props! |

npx expo install expo-localization @react-native-async-storage/async-storage npx expo install @react-native-picker/picker 'This provides an easy way to create drop down menu list'

Peer Dependencies

| Package | Version | |---------|---------| | expo-localization | >=56.0.6 | | expo-router | >=56.2.9 | | @react-native-async-storage/async-storage | >=2.2.0 | | react | >=19.2.3 | | react-native | >=0.85.3 |


Compatibility

| Expo SDK | React Native | Status | |----------|-------------|--------| | 56 | 0.81 | ✅ Tested | | 54+ | 0.76+ | ✅ Should work | | Below 52 | Below 0.76 | ⚠️ Not tested |


Troubleshooting

🔍 Troubleshooting & Common Fixes

1. Error: Cannot find module '../../locales/fr.json'

This happens when the relative path inside your require() statement doesn't accurately match your file's location. Because Expo apps use nested file routing, your path depends on how deep your component sits.

  • If your file is in src/app/index.jsx: Use two sets of dots:
    fr: require("../locales/fr.json")
  • If your file is deeply nested (e.g., src/app/(tabs)/settings.jsx): You need to step up three full levels to exit the routing groups and reach your locales:
    fr: require("../../../locales/fr.json")
  • Best Fix: Use Expo's path aliasing shortcut to bypass dot-counting altogether:
    fr: require("@/locales/fr.json") // or "~/locales/fr.json"

2. New languages are not showing up in the app picker

If you added a new language code (like ,de,ja) to your package.json script and ran npm run translate, the .json files are physically created on your hard drive, but your app doesn't know they exist yet.

  • Fix: You must explicitly register the new files inside your MY_APP_BUNDLES object where you initialize the provider:
    const MY_APP_BUNDLES = {
      fr: require("../locales/fr.json"),
      es: require("../locales/es.json"),
      de: require("../locales/de.json"), // <-- Add this!
      ja: require("../locales/ja.json"), // <-- Add this!
    };

3. I updated my code or added a language, but nothing changed

The Metro Bundler heavily caches local asset files (like JSON) to keep your development environment fast. Sometimes it completely misses the fact that a build script modified or generated new translation files.

  • Fix: Kill your running terminal process (Ctrl + C) and restart Expo while forcing it to completely purge its memory cache:
    npx expo start --clear

4. Error: X-API-Key is missing or invalid

This occurs if the translation script cannot read your AI proxy credential during the build phase.

  • Fix (Mac/Linux): Make sure your variable is assigned in your local project terminal session before executing, or prefix the run:
    EXPO_PUBLIC_POLYGLOT_API_KEY="your_key_here" npm run translate
  • Fix (Windows PowerShell): If your package.json script fails to parse $EXPO_PUBLIC_POLYGLOT_API_KEY, explicitly replace the variable name in your script line with your actual string token, or pass it via standard Windows environment parameters.

5. My translated text is running too long and breaking buttons

Certain languages use significantly longer phrase layouts than English, which can cause text strings to overlap design elements or wrap awkwardly.

  • Fix: Avoid using regular Expo <Text> components for strings that change layout length. Always pass your text into the package's declarative layout guard with the autoShrink parameter activated:
    <TranslateText autoShrink style={{ fontSize: 18 }}>
      Your long dynamic text string here
    </TranslateText>
    This automatically downsizes the font-size micro-proportionally to keep your UI looking flawless on small screens.

📂 Repository

https://github.com/Inspire00/expo-polyglot-ai


📄 License

MIT © Vincent


🪵 Changelog

1.0.0 (2026-06-14)

  • 🎉 Initial release
  • TranslationProvider container with dynamic engine injection
  • Incremental extract-strings.js pipeline parser tool
  • Fully resumable translate-bundle.js compiler with context-aware Gemini AI integrations
  • <TranslateText> UI protection layout layer with native autoShrink downscaling
  • Full utility-class interoperability supporting NativeWind / Tailwind CSS
  • Local AsyncStorage caching systems optimized for zero dynamic network latency
  • Secure environment token proxy layer preventing public API credential exposure