appscript.dev
Automation Intermediate Gmail

Auto-label and route emails by language

Detect the message language and send each thread to the localised support owner.

Published Jan 20, 2026

Northwind’s support inbox is shared, but the team is not. Clients in Berlin email in German, Madrid writes in Spanish, and Paris in French. When every thread lands in one undifferentiated pile, the wrong person opens it first, skims a language they only half-read, and either replies slowly or hands it on. The delay is small per message and large across a week.

This script reads each new support thread, detects the language of the first message, and applies a lang/... label that matches. A saved Gmail filter or a quick glance then routes each thread to the teammate who speaks that language — Hans takes lang/de, María takes lang/es, and English stays with Awadesh. The inbox sorts itself before anyone opens it.

What you’ll need

  • A Gmail account where support mail already carries a support label, applied by a filter on arrival.
  • Language labels created up front: lang/de, lang/es, lang/fr, and lang/en. The script creates any that are missing, but making them yourself lets you assign colours and filters first.
  • Optional: a Gmail filter per language label that forwards or stars the thread for the right owner.

The script

// The Gmail label your arrival filter applies to support mail.
const SUPPORT_LABEL = 'support';

// Detected ISO language code -> the label to apply.
// Add a row here for any language you want to route.
const ROUTES = {
  de: 'lang/de',
  es: 'lang/es',
  fr: 'lang/fr',
};

// Fallback label for English and anything we can't detect confidently.
const DEFAULT_LABEL = 'lang/en';

// How many characters of the message to sample for detection.
// The opening lines carry enough signal — full bodies waste quota.
const SAMPLE_CHARS = 500;

/**
 * Finds unread support threads with no language label yet, detects the
 * language of the first message, and applies the matching lang/... label.
 */
function routeByLanguage() {
  // 1. Search for support threads that are still unlabelled and unread.
  //    Excluding every lang/... label keeps already-routed threads out.
  const query =
    'label:' + SUPPORT_LABEL +
    ' -label:' + DEFAULT_LABEL +
    Object.values(ROUTES).map((l) => ' -label:' + l).join('') +
    ' is:unread';
  const threads = GmailApp.search(query);

  if (!threads.length) {
    Logger.log('No unrouted support threads — nothing to do.');
    return;
  }

  // 2. Walk each thread and label it by detected language.
  for (const thread of threads) {
    // Sample the start of the first message — enough for a reliable guess.
    const body = thread.getMessages()[0]
      .getPlainBody()
      .slice(0, SAMPLE_CHARS);

    // 3. LanguageApp.detect() returns an ISO code ('en', 'de', 'es', ...).
    //    Fall back to English if it returns nothing.
    const code = LanguageApp.detect(body) || 'en';

    // 4. Map the code to a label, defaulting to English for anything
    //    we don't have a route for.
    const labelName = ROUTES[code] || DEFAULT_LABEL;
    const label = getOrCreateLabel(labelName);
    thread.addLabel(label);
  }

  Logger.log('Routed ' + threads.length + ' support threads.');
}

/**
 * Returns the named Gmail label, creating it if it does not exist yet.
 */
function getOrCreateLabel(name) {
  return GmailApp.getUserLabelByName(name) || GmailApp.createLabel(name);
}

How it works

  1. routeByLanguage builds a search query for threads that carry the support label, are still unread, and have no lang/... label yet — so each thread is routed exactly once.
  2. If the search finds nothing, it logs a message and stops, avoiding wasted work on an empty inbox.
  3. For each thread it reads the first message and takes the opening 500 characters (SAMPLE_CHARS). Those lines carry enough signal, and trimming keeps the LanguageApp calls light.
  4. LanguageApp.detect returns an ISO language code. If it returns nothing — common for very short messages — the code falls back to en.
  5. The code is looked up in ROUTES to get a label name; anything not in the map (including English) falls through to DEFAULT_LABEL.
  6. getOrCreateLabel fetches the label, creating it on the fly if it is missing, and the thread is labelled.

Example run

The support inbox holds three unread, unrouted threads:

SubjectFirst lineDetectedLabel applied
Rechnung Frage”Guten Tag, ich habe eine Frage zur…“delang/de
Pedido retrasado”Hola, mi pedido no ha llegado…“eslang/es
Login issue”Hi team, I can’t sign in to…“enlang/en

After a run, each thread carries a single language label, and Hans, María, and Awadesh each see only the threads they can act on.

Trigger it

Run this on a short timer so threads are sorted within a few minutes of arriving:

  1. In the Apps Script editor, open Triggers (the clock icon).
  2. Click Add Trigger.
  3. Choose routeByLanguage, event source Time-driven, and a Minutes timer every 5 or 10 minutes.

A 10-minute interval keeps you well inside Gmail’s daily read quota even for a busy inbox.

Watch out for

  • One-word or very short emails detect unreliably. The script already defaults to lang/en when detection fails, which is safer than guessing — but expect the odd terse message to land in the English pile.
  • Quoted replies and signatures can skew detection. Sampling only the first 500 characters helps, but a German reply to an English thread may still read as mixed; check the first message, not the whole thread.
  • LanguageApp shares your account’s daily URL Fetch / translation quota. A 5-minute timer over a normal support volume is fine; re-scanning thousands of threads at once is not.
  • The script labels but does not move or assign threads. Pair each lang/... label with a Gmail filter if you want the thread starred, forwarded, or filed for its owner automatically.

Related