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:
- Step 1: Extension Package - Add
_localesfolder to your extension and updatemanifest.json. This automatically localizes your extension name and short description on the Chrome Web Store. - Step 2: Store Descriptions - Manually add full store descriptions via the Developer Dashboard for each language.
_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:
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:
{
"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"
}
}| Field | Description | Character Limit | Auto-localized? |
|---|---|---|---|
appName | Extension name | 75 characters | Yes, via manifest |
shortDesc | Short description | 132 characters | Yes, via manifest |
storeDesc | Full store description | No hard limit | No, 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:
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_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__"
}
}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. First, Chrome looks for an exact match (e.g.,
es_419for Latin American Spanish) - 2. If not found, it tries the language without region (e.g.,
es) - 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.
Open Chrome Web Store Developer Dashboard
Go to Developer Dashboard and select your extension.
Navigate to Store Listing
Click on "Store listing" in the left sidebar.
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).
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.
Repeat for all languages
Select each language from the dropdown and paste the corresponding storeDesc translation.
Save and publish
Click "Save draft" after each language, then submit for review when all languages are complete.
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 → Inspect → Console tab
Steps
Go to the Store listing page in your Developer Dashboard.
Open Developer Tools and click the Console tab.
Copy the script below, paste it into the Console, and press Enter.
A widget appears in the bottom-right corner of the page. Click 📂 Load _locales folder and select your _locales folder.
Wait while all languages are uploaded automatically. Progress is shown in the widget.
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.
Click Save draft, then Submit for review.
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:
# 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=deTroubleshooting 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:
Related Articles
Written by

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