Back to Articles
Developer Guide

How to Integrate Translations into Your Chrome Extension

A comprehensive guide to using your exported translations from Localeship in your Chrome extension project.

Ruslan Saifullin
Ruslan Saifullin
Dec 24, 2024·8 min read

Introduction

After translating your Chrome extension listing with Localeship, you'll receive a ZIP file containing translations in Chrome's official _locales format. This guide explains how to integrate these translations into your project.

The integration process has two main steps:

  1. Step 1: Extension Package - Add _locales folder to your extension and update manifest.json. This automatically localizes your extension name and short description on the Chrome Web Store.
  2. Step 2: Store Descriptions - Manually add full store descriptions via the Developer Dashboard for each language.
When you upload an extension with _locales folder, Chrome Web Store automatically reads appName and shortDesc from your messages.json files. However, the full store description (storeDesc) must be added manually in the Developer Dashboard.

Understanding the Export

When you export translations from Localeship, you receive a ZIP file with this structure:

Export ZIP Structure
your-extension-locales-2024-12-24.zip
└── _locales/
    ├── en/
    │   └── messages.json
    ├── es/
    │   └── messages.json
    ├── fr/
    │   └── messages.json
    ├── de/
    │   └── messages.json
    ├── ja/
    │   └── messages.json
    ├── zh_CN/
    │   └── messages.json
    └── ... (up to 52 languages)

Each messages.json file contains three translated fields:

_locales/es/messages.json (Spanish example)
{
  "appName": {
    "message": "Mi Extensión Increíble"
  },
  "shortDesc": {
    "message": "Mejora tu navegación con funciones potentes"
  },
  "storeDesc": {
    "message": "Mi Extensión Increíble transforma tu experiencia de navegación...\n\nCaracterísticas principales:\n• Función uno\n• Función dos\n• Función tres"
  }
}
FieldDescriptionCharacter LimitAuto-localized?
appNameExtension name75 charactersYes, via manifest
shortDescShort description132 charactersYes, via manifest
storeDescFull store descriptionNo hard limitNo, manual upload

Step 1: Extension Package Localization

Add the _locales folder to your extension package and update your manifest.json. When you upload to the Chrome Web Store, it will automatically use the localized extension name and short description.

1.1 Add _locales to your extension

Copy the entire _locales folder from the ZIP into your extension's root directory:

Extension Directory Structure
your-extension/
├── manifest.json
├── popup.html
├── popup.js
├── background.js
├── icons/
│   └── icon128.png
└── _locales/
    ├── en/
    │   └── messages.json
    ├── es/
    │   └── messages.json
    ├── fr/
    │   └── messages.json
    └── .../

1.2 Update manifest.json

Add default_locale and use __MSG_*__ placeholders for name and description:

manifest.json
{
  "manifest_version": 3,
  "name": "__MSG_appName__",
  "description": "__MSG_shortDesc__",
  "version": "1.0.0",
  "default_locale": "en",
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  },
  "action": {
    "default_popup": "popup.html",
    "default_title": "__MSG_appName__"
  }
}
The default_locale field is required when you have a _locales folder. Without it, Chrome will reject the extension.

1.3 Upload to Chrome Web Store

Package your extension (ZIP the folder) and upload it to the Chrome Web Store Developer Dashboard. Chrome will automatically:

  • • Read extension names from each _locales/[lang]/messages.json
  • • Read short descriptions from each locale
  • • Display the correct language version to users based on their browser settings

How Message Lookup Works

Chrome automatically selects the appropriate translation based on the user's browser language:

  1. 1. First, Chrome looks for an exact match (e.g., es_419 for Latin American Spanish)
  2. 2. If not found, it tries the language without region (e.g., es)
  3. 3. If still not found, it falls back to default_locale

Step 2: Add Store Descriptions

The full store description (storeDesc) is not read from _locales. You must manually add it for each language in the Developer Dashboard.

1

Open Chrome Web Store Developer Dashboard

Go to Developer Dashboard and select your extension.

2

Navigate to Store Listing

Click on "Store listing" in the left sidebar.

3

Select a language

At the top of the Store Listing page, you'll see a language dropdown. Select the language you want to add (e.g., Spanish, French, German).

4

Add the detailed description

Open the corresponding messages.json file (e.g., _locales/es/messages.json for Spanish) and copy the storeDesc.message content into the "Detailed description" field.

5

Repeat for all languages

Select each language from the dropdown and paste the corresponding storeDesc translation.

6

Save and publish

Click "Save draft" after each language, then submit for review when all languages are complete.

You can also add localized screenshots and promotional videos for each language in the same Store Listing section.
Doing the above manually for 52 languages takes a long time. Use the automation script below to upload all descriptions in one click — no extra tools required.

Advanced: Automate with a Browser Script

Instead of switching languages one by one, paste this script into your browser's Developer Console on the Store Listing page. It injects a small widget that automatically selects each language and fills in the description from your _locales folder.

How to open Developer Tools on the Store Listing page

  • Windows / Linux: press F12 or Ctrl + Shift + J
  • Mac: press Cmd + Option + J
  • Or: right-click anywhere on the page → InspectConsole tab

Steps

1

Go to the Store listing page in your Developer Dashboard.

2

Open Developer Tools and click the Console tab.

3

Copy the script below, paste it into the Console, and press Enter.

4

A widget appears in the bottom-right corner of the page. Click 📂 Load _locales folder and select your _locales folder.

5

Wait while all languages are uploaded automatically. Progress is shown in the widget.

6

Once done, use the Current editing language dropdown at the top of the page to spot-check a few languages and confirm their descriptions are filled in correctly.

7

Click Save draft, then Submit for review.

store-desc-uploader.js — paste in browser Console
const UPLOAD_DELAY_MS = 500;

const XPATH = {
  languageDropdown: "//h3[text()='Current editing language']/../../div[2]//div[@jsshadow]/div/div",
  languageList:     "//ul[@aria-label='Language']",
  descriptionField: "//textarea[@maxlength='16000']",
};

// Extensible remap table instead of a hardcoded if-block
const LOCALE_CODE_REMAPS = { he: 'iw' };

// --- Utilities ---

function xpathNode(expression) {
  return document.evaluate(
    expression, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null
  ).singleNodeValue;
}

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

function normalizeCode(rawCode) {
  const dashed = rawCode.replace('_', '-');
  return LOCALE_CODE_REMAPS[dashed] ?? dashed;
}

function extractLocaleCode(relativePath) {
  const match = relativePath.match(/_locales\/([^/]+)\/messages\.json/);
  return match?.[1] ?? null;
}

// --- File parsing ---

async function parseLocaleFiles(fileList) {
  const locales = {};

  const messageFiles = Array.from(fileList).filter(
    f => f.name === 'messages.json' && f.type === 'application/json'
  );

  for (const file of messageFiles) {
    const code = extractLocaleCode(file.webkitRelativePath);
    if (!code) continue;

    try {
      const parsed = JSON.parse(await file.text());
      if (parsed.storeDesc?.message) {
        locales[code] = parsed.storeDesc.message;
      } else {
        console.warn(`[${code}] storeDesc.message not found`);
      }
    } catch (err) {
      console.error(`[${code}] JSON parse error:`, err);
    }
  }

  return locales;
}

// --- DOM interactions ---

async function selectLanguage(code) {
  const dropdown = xpathNode(XPATH.languageDropdown);
  if (!dropdown) throw new Error('Language dropdown not found');
  dropdown.click();

  const list = xpathNode(XPATH.languageList);
  const option = Array.from(list?.children ?? [])
    .find(el => el.tagName === 'LI' && el.getAttribute('data-value') === code);

  if (!option) throw new Error(`No list item for locale: ${code}`);
  option.click();
}

async function fillDescription(text) {
  const textarea = xpathNode(XPATH.descriptionField);
  if (!textarea) throw new Error('Description textarea not found');

  textarea.dispatchEvent(new Event('focus'));
  textarea.value = text;
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
}

// --- Upload orchestration ---

async function uploadLocales(locales) {
  for (const [rawCode, description] of Object.entries(locales)) {
    const code = normalizeCode(rawCode);
    console.log(`Uploading: ${code}`);

    try {
      await selectLanguage(code);
      await delay(UPLOAD_DELAY_MS);
      await fillDescription(description);
      await delay(UPLOAD_DELAY_MS);
    } catch (err) {
      console.error(`[${code}] Skipped — ${err.message}`);
    }
  }
}

// --- Entry point ---

function mountFilePicker() {
  // --- Keyframes ---
  if (!document.getElementById('localePickerStyle')) {
    const style = document.createElement('style');
    style.id = 'localePickerStyle';
    style.textContent = `
      @keyframes localePickerPulse {
        0%, 100% { box-shadow: 0 4px 15px rgba(124,58,237,0.5); }
        50%       { box-shadow: 0 4px 24px rgba(219,39,119,0.7); }
      }
      @keyframes saveDraftGlow {
        0%, 100% { box-shadow: 0 0 0 0 rgba(5,150,105,0.7); }
        50%       { box-shadow: 0 0 0 6px rgba(5,150,105,0); }
      }
      @keyframes stepFadeIn {
        from { opacity: 0; transform: translateY(-4px); }
        to   { opacity: 1; transform: translateY(0); }
      }
    `;
    document.head.append(style);
  }

  // --- Container ---
  const container = document.createElement('div');
  container.style.cssText = `
    position: fixed;
    bottom: 20px;
    right: 12px;
    z-index: 2147483647;
    font-family: system-ui, -apple-system, sans-serif;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    width: 230px;
  `;

  // --- Button label ---
  const label = document.createElement('label');
  label.htmlFor = 'localePicker';
  label.style.cssText = `
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 18px;
    background: linear-gradient(135deg, #7c3aed, #db2777);
    color: #fff;
    border-radius: 10px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.3px;
    cursor: pointer;
    box-shadow: 0 4px 15px rgba(124,58,237,0.5);
    user-select: none;
    white-space: nowrap;
    transition: filter 0.15s, box-shadow 0.15s;
    animation: localePickerPulse 2.5s ease-in-out infinite;
  `;
  label.textContent = '📂 Load _locales folder';
  label.onmouseenter = () => { label.style.filter = 'brightness(1.15)'; };
  label.onmouseleave = () => { label.style.filter = ''; };

  // --- Instructions panel ---
  const panel = document.createElement('div');
  panel.style.cssText = `
    background: rgba(15,15,20,0.88);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 12px;
    padding: 12px 14px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    display: flex;
    flex-direction: column;
    gap: 8px;
  `;

  const stepsData = [
    { id: 'step1', label: 'Select folder',  text: 'Click the button above and open your _locales folder' },
    { id: 'step2', label: 'Uploading',      text: 'Uploading languages…' },
    { id: 'step3', label: 'Save & submit',  text: 'Click "Save draft", then "Submit for review"' },
  ];

  const stepEls = {};

  stepsData.forEach((s, i) => {
    const row = document.createElement('div');
    row.style.cssText = `
      display: flex;
      align-items: flex-start;
      gap: 10px;
      animation: stepFadeIn 0.3s ease both;
      animation-delay: ${i * 0.07}s;
    `;

    const badge = document.createElement('div');
    badge.style.cssText = `
      min-width: 20px;
      height: 20px;
      border-radius: 50%;
      background: rgba(255,255,255,0.12);
      color: rgba(255,255,255,0.4);
      font-size: 11px;
      font-weight: 700;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 1px;
      transition: background 0.3s, color 0.3s;
    `;
    badge.textContent = i + 1;

    const textEl = document.createElement('div');
    textEl.style.cssText = `
      font-size: 11.5px;
      line-height: 1.55;
      color: rgba(255,255,255,0.35);
      transition: color 0.3s;
    `;
    const labelSpan = document.createElement('span');
    labelSpan.style.cssText = 'font-weight:600;display:block;margin-bottom:1px;';
    labelSpan.textContent = s.label;
    textEl.append(labelSpan);
    textEl.append(document.createTextNode(s.text));

    row.append(badge, textEl);
    panel.append(row);
    stepEls[s.id] = { row, badge, textEl };
  });

  function activateStep(id) {
    Object.entries(stepEls).forEach(([key, { badge, textEl }]) => {
      const isActive = key === id;
      badge.style.background  = isActive ? 'linear-gradient(135deg,#7c3aed,#db2777)' : 'rgba(255,255,255,0.12)';
      badge.style.color        = isActive ? '#fff' : 'rgba(255,255,255,0.4)';
      textEl.style.color       = isActive ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.35)';
    });
  }

  activateStep('step1');

  // --- File input ---
  const picker = document.createElement('input');
  picker.id = 'localePicker';
  picker.type = 'file';
  picker.setAttribute('webkitdirectory', '');
  picker.setAttribute('multiple', '');
  picker.style.display = 'none';

  picker.addEventListener('change', async ({ target }) => {
    // Step 2: uploading
    activateStep('step2');
    label.style.animation = 'none';
    label.style.background = 'linear-gradient(135deg, #d97706, #ea580c)';
    label.style.boxShadow  = '0 4px 15px rgba(217,119,6,0.5)';
    label.style.cursor     = 'default';
    label.onmouseenter = label.onmouseleave = null;

    const locales = await parseLocaleFiles(target.files);
    const total = Object.keys(locales).length;
    let done = 0;

    const { textEl: step2text } = stepEls['step2'];

    const originalEntries = Object.entries(locales);
    for (const [rawCode, description] of originalEntries) {
      const code = normalizeCode(rawCode);
      step2text.replaceChildren();

      const uploadingLabel = document.createElement('span');
      uploadingLabel.style.cssText = 'font-weight:600;display:block;margin-bottom:1px;';
      uploadingLabel.textContent = 'Uploading';

      const uploadingLine = document.createTextNode(`Uploading ${code}…`);

      const countSpan = document.createElement('span');
      countSpan.style.cssText = 'display:block;color:rgba(255,255,255,0.5);font-size:11px;margin-top:2px;';
      countSpan.textContent = `${done} / ${total} done`;

      step2text.append(uploadingLabel, uploadingLine, countSpan);

      try {
        await selectLanguage(code);
        await delay(UPLOAD_DELAY_MS);
        await fillDescription(description);
        await delay(UPLOAD_DELAY_MS);
      } catch (err) {
        console.error(`[${code}] Skipped — ${err.message}`);
      }
      done++;
    }

    // Step 3: done
    activateStep('step3');
    label.style.background = 'linear-gradient(135deg, #059669, #0284c7)';
    label.style.boxShadow  = '0 4px 15px rgba(5,150,105,0.5)';
    label.textContent      = `✅ Done — ${done} languages uploaded`;

    // Pulse the real "Save draft" button on the page to guide user
    const saveDraftBtn = [...document.querySelectorAll('button')]
      .find(b => b.textContent.trim() === 'Save draft');
    if (saveDraftBtn) {
      saveDraftBtn.style.animation = 'saveDraftGlow 1s ease-in-out infinite';
      saveDraftBtn.style.borderRadius = '6px';
    }
  });

  container.append(label, picker, panel);
  document.documentElement.append(container);
}

mountFilePicker();

Testing Your Localized Extension

The easiest way to test your localized Chrome Web Store listing is by using the hl URL parameter.

Preview Different Languages

Add ?hl=LANGUAGE_CODE to your extension's Chrome Web Store URL:

Testing URLs
# Your extension URL
https://chromewebstore.google.com/detail/your-extension/abcdefghijklmnop

# Test Spanish
https://chromewebstore.google.com/detail/your-extension/abcdefghijklmnop?hl=es

# Test Russian
https://chromewebstore.google.com/detail/your-extension/abcdefghijklmnop?hl=ru

# Test Japanese
https://chromewebstore.google.com/detail/your-extension/abcdefghijklmnop?hl=ja

# Test German
https://chromewebstore.google.com/detail/your-extension/abcdefghijklmnop?hl=de
This method works immediately after your extension is published - no need to change your browser settings or restart Chrome.

Troubleshooting Common Issues

"default_locale" is required

If you have a _locales folder, you must add "default_locale": "en" to your manifest.json.

Missing _locales folder in build

Check that your build process includes the _locales folder in the output. If you're using a bundler (Webpack, Vite, etc.), ensure it copies the folder to the dist/build directory. Verify all translations are present inside.

Translation not showing

Check that the locale code matches exactly (e.g., zh_CN vs zh-CN). Chrome uses underscores, not hyphens.

Special characters displaying incorrectly

Ensure your messages.json files are saved with UTF-8 encoding. All Localeship exports use UTF-8 by default.

Store description not localized

Remember that storeDesc must be added manually via the Developer Dashboard. It is not automatically read from _locales.

Best Practices

Keep your source language updated

When you update your English listing, re-translate to keep all languages in sync.

Test high-priority languages first

Focus on languages with the most potential users (Spanish, German, French, Japanese, Chinese).

Monitor user feedback by locale

Check reviews filtered by language to catch any translation issues reported by users.

Use Localeship's preview feature

Review all translations in your dashboard before exporting. Edit any text that doesn't look right.

Official References

For more detailed information, refer to Chrome's official documentation:

Written by

Ruslan Saifullin
Ruslan Saifullin

Full-stack developer · 14 years of experience · 5 Chrome extensions shipped

Ruslan Saifullin is the founder of Localeship and a full-stack developer with 14 years of experience building with PHP and Go. He holds a Bachelor’s degree in Software Engineering and has built and launched 5 Chrome extensions, growing one to $200 MRR — giving him firsthand insight into what it takes to build, localize, and market extensions on the Chrome Web Store.

Ready to localize your extension?

Get 3 free translations to start. No credit card required.

Start translating free