Internationalizing Your Chrome Extension — Developer Guide
18 min readInternationalizing Your Chrome Extension
Overview
Internationalization (i18n) is essential for reaching a global audience with your Chrome extension. Chrome provides a built-in i18n system that requires no permissions and supports over 50 languages, including right-to-left (RTL) scripts like Arabic, Hebrew, and Persian.
This tutorial covers the complete workflow for adding multilingual support to your extension, from setting up the _locales directory to publishing localized listings in the Chrome Web Store.
Prerequisites
- A Chrome Extension project with a
manifest.jsonfile - Basic understanding of JSON format
- Familiarity with JavaScript/TypeScript
Step 1: Set Up the _locales Directory
Create the _locales directory in your extension’s root folder. Each supported language gets its own subdirectory named using the ISO 639-1 code:
my-extension/
├── _locales/
│ ├── en/messages.json # English (default)
│ ├── es/messages.json # Spanish
│ ├── fr/messages.json # French
│ ├── de/messages.json # German
│ ├── ja/messages.json # Japanese
│ ├── ar/messages.json # Arabic (RTL)
│ └── zh_CN/messages.json # Chinese (Simplified)
├── manifest.json
├── background.js
├── popup.html
└── styles.css
Configure Default Locale
Add the default_locale field to your manifest.json:
{
"manifest_version": 3,
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"default_locale": "en",
"version": "1.0.0"
}
Important: The default_locale field is required if you use __MSG_*__ placeholders in your manifest. It specifies the fallback locale when a user’s language isn’t available.
Step 2: Create messages.json Files
Each locale directory must contain a messages.json file with translation strings.
English (Default) — _locales/en/messages.json
{
"extension_name": {
"message": "My Weather Extension",
"description": "The name of the extension"
},
"extension_description": {
"message": "Get real-time weather updates for your location",
"description": "Extension description"
},
"menu_settings": {
"message": "Settings",
"description": "Settings menu item"
},
"menu_about": {
"message": "About",
"description": "About menu item"
},
"greeting": {
"message": "Hello, $1!",
"description": "Greeting message with user name"
},
"weather_temp": {
"message": "The temperature in $CITY$ is $TEMP$°C",
"description": "Weather report message",
"placeholders": {
"city": {
"content": "$1",
"example": "Tokyo"
},
"temp": {
"content": "$2",
"example": "22"
}
}
},
"items_count": {
"message": "You have $COUNT$ item(s)",
"description": "Item count message",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"button_save": {
"message": "Save",
"description": "Save button text"
},
"button_cancel": {
"message": "Cancel",
"description": "Cancel button text"
}
}
Spanish — _locales/es/messages.json
{
"extension_name": {
"message": "Mi Extensión del Clima",
"description": "The name of the extension"
},
"extension_description": {
"message": "Obtén actualizaciones del clima en tiempo real para tu ubicación",
"description": "Extension description"
},
"menu_settings": {
"message": "Configuración",
"description": "Settings menu item"
},
"menu_about": {
"message": "Acerca de",
"description": "About menu item"
},
"greeting": {
"message": "¡Hola, $1!",
"description": "Greeting message with user name"
},
"weather_temp": {
"message": "La temperatura en $CITY$ es $TEMP$°C",
"description": "Weather report message",
"placeholders": {
"city": {
"content": "$1",
"example": "Tokio"
},
"temp": {
"content": "$2",
"example": "22"
}
}
},
"items_count": {
"message": "Tienes $COUNT$ elemento(s)",
"description": "Item count message",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"button_save": {
"message": "Guardar",
"description": "Save button text"
},
"button_cancel": {
"message": "Cancelar",
"description": "Cancel button text"
}
}
Arabic (RTL) — _locales/ar/messages.json
{
"extension_name": {
"message": "امتداد الطقس الخاص بي",
"description": "The name of the extension"
},
"extension_description": {
"message": "احصل على تحديثات الطقس في الوقت الفعلي لموقعك",
"description": "Extension description"
},
"menu_settings": {
"message": "الإعدادات",
"description": "Settings menu item"
},
"menu_about": {
"message": "حول",
"description": "About menu item"
},
"greeting": {
"message": "مرحباً، $1!",
"description": "Greeting message with user name"
},
"weather_temp": {
"message": "درجة الحرارة في $CITY$ هي $TEMP$°م",
"description": "Weather report message",
"placeholders": {
"city": {
"content": "$1",
"example": "طوكيو"
},
"temp": {
"content": "$2",
"example": "22"
}
}
},
"items_count": {
"message": "لديك $COUNT$ عنصر(عناصر)",
"description": "Item count message",
"placeholders": {
"count": {
"content": "$1",
"example": "5"
}
}
},
"button_save": {
"message": "حفظ",
"description": "Save button text"
},
"button_cancel": {
"message": "إلغاء",
"description": "Cancel button text"
}
}
Step 3: Use Messages in Your Extension
In manifest.json
Reference messages using the __MSG_key_name__ syntax:
{
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__"
}
In HTML Files
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1 data-i18n="extension_name"></h1>
<button id="settings" data-i18n="menu_settings"></button>
<button id="save" data-i18n="button_save"></button>
<button id="cancel" data-i18n="button_cancel"></button>
<p id="greeting"></p>
<p id="weather"></p>
<script src="popup.js"></script>
</body>
</html>
In JavaScript/TypeScript
The chrome.i18n API provides methods for retrieving translated strings:
// popup.js
// Get a simple message
const extensionName = chrome.i18n.getMessage("extension_name");
console.log(extensionName); // "My Weather Extension"
// Get message with positional arguments
const greeting = chrome.i18n.getMessage("greeting", ["John"]);
console.log(greeting); // "Hello, John!"
// Get message with named placeholders
const weather = chrome.i18n.getMessage("weather_temp", ["Tokyo", "22"]);
console.log(weather); // "The temperature in Tokyo is 22°C"
// Get message with array of arguments
const items = chrome.i18n.getMessage("items_count", [5]);
console.log(items); // "You have 5 item(s)"
Automatic HTML Translation
Create a helper function to translate all elements with data-i18n attributes:
// i18n.js - Helper for translating HTML elements
function translatePage() {
// Find all elements with data-i18n attribute
const elements = document.querySelectorAll("[data-i18n]");
elements.forEach((element) => {
const messageKey = element.getAttribute("data-i18n");
const message = chrome.i18n.getMessage(messageKey);
if (message) {
element.textContent = message;
}
});
}
// Translate on page load
document.addEventListener("DOMContentLoaded", translatePage);
// Also translate when the language might change
chrome.i18n.getAcceptLanguages((languages) => {
console.log("Accepted languages:", languages);
});
Step 4: Handle RTL Languages
For languages like Arabic, Hebrew, and Persian that read right-to-left, you need to adjust your CSS.
Detect RTL in JavaScript
// Check if the current locale is RTL
function isRTL() {
const language = chrome.i18n.getUILanguage();
// Common RTL language codes
const rtlLanguages = ["ar", "he", "fa", "ur"];
return rtlLanguages.includes(language.split("-")[0]);
}
// Apply RTL class to body
if (isRTL()) {
document.body.classList.add("rtl");
}
CSS for RTL Support
/* styles.css */
/* Default LTR styles */
.container {
direction: ltr;
text-align: left;
}
.button {
margin-right: 10px;
padding: 8px 16px;
}
.icon {
margin-right: 8px;
}
/* RTL-specific styles */
body.rtl .container {
direction: rtl;
text-align: right;
}
body.rtl .button {
margin-right: 0;
margin-left: 10px;
}
body.rtl .icon {
margin-right: 0;
margin-left: 8px;
}
/* Use logical properties (recommended) */
.flex-container {
display: flex;
gap: 16px; /* Replaces margin/padding in most cases */
}
.card {
padding: 16px;
border-start-start-radius: 8px; /* Logical border-radius */
border-end-end-radius: 8px;
}
Using dir Attribute
Set the dir attribute dynamically:
function updateDocumentDirection() {
const language = chrome.i18n.getUILanguage();
const rtlLanguages = ["ar", "he", "fa", "ur", "yi", "ps"];
const isRTL = rtlLanguages.includes(language.split("-")[0]);
document.documentElement.dir = isRTL ? "rtl" : "ltr";
document.documentElement.lang = language;
}
document.addEventListener("DOMContentLoaded", updateDocumentDirection);
Step 5: Understand Locale Fallback Chain
Chrome uses a fallback chain when a translation isn’t available in the user’s preferred language:
- User’s preferred language (e.g.,
fr_FR) - Language without region (e.g.,
fr) - Default locale (specified in
default_locale) - English (en) as final fallback
Example Fallback Chain
If user’s language is fr_FR and you only have fr and en:
fr_FR → fr → en (default_locale) → en (hardcoded)
Check Available Languages
// Get languages accepted by the browser
chrome.i18n.getAcceptLanguages((languages) => {
console.log("Accept languages:", languages);
// Output: ["en-US", "en", "fr-FR", "fr", "es", "es-419"]
});
// Get the current UI language
const uiLanguage = chrome.i18n.getUILanguage();
console.log("UI Language:", uiLanguage);
// Output: "fr-FR"
Handle Missing Translations Gracefully
function getMessageSafe(key, substitutions) {
try {
return chrome.i18n.getMessage(key, substitutions);
} catch (error) {
console.warn(`Missing translation for key: ${key}`);
return key; // Fall back to showing the key
}
}
Step 6: Use Predefined Messages
Chrome provides built-in messages for common UI elements:
| Message Key | Description | Example Output |
|---|---|---|
@@extension_id |
Extension’s unique ID | knpkdiapbgcjkpjfjgjhenjhhplbjhn |
@@ui_locale |
Current locale | en |
@@bidi_dir |
Text direction | ltr or rtl |
@@bidi_reversed_dir |
Reversed text direction | rtl or ltr |
@@bidi_start_edge |
Start edge position | left or right |
@@bidi_end_edge |
End edge position | right or left |
Using Predefined Messages
// Get extension ID (useful for dynamic resource URLs)
const extensionId = chrome.i18n.getMessage("@@extension_id");
console.log(extensionId);
// Get text direction
const direction = chrome.i18n.getMessage("@@bidi_dir");
console.log(direction); // "ltr" or "rtl"
// Using in CSS via JavaScript
document.documentElement.setAttribute("dir",
chrome.i18n.getMessage("@@bidi_dir")
);
Step 7: Testing Locales
Test in Chrome
- Open
chrome://extensions/ - Enable Developer mode
- Click Load unpacked and select your extension folder
- Click the extension icon → puzzle piece → manage
- Under “Language”, select a different language
- Restart Chrome or reload the extension
Programmatic Testing
// Test different languages
async function testLanguage(locale) {
// Create a temporary element to test translations
const testElement = document.createElement("div");
testElement.setAttribute("data-i18n", "extension_name");
document.body.appendChild(testElement);
translatePage();
console.log(`Locale ${locale}:`, testElement.textContent);
document.body.removeChild(testElement);
}
// Test multiple locales
["en", "es", "fr", "de", "ar", "zh_CN"].forEach(testLanguage);
Using Chrome Flags
You can force Chrome to use a specific language for testing:
- Right-click Chrome shortcut
- Add
--lang=esto the target path - Restart Chrome
Extension Reloader Extension
Install the Extension Reloader extension to quickly reload your extension during development.
Step 8: Chrome Web Store Localized Listings
When publishing to the Chrome Web Store, you can provide localized listings for different regions and languages.
Store Listing Fields to Localize
- Title - Extension name
- Description - Detailed description
- Short Description - Brief summary
- Promotional Graphics - Store banners (may need region-specific images)
Upload Localized Screenshots
- Go to Chrome Web Store Developer Dashboard
- Select your extension
- Go to Store Listing
- Scroll to Localized listings
- Add translations for each target language/region
Example: English Store Listing
Title: My Weather Extension
Description: Get real-time weather updates for your location.
Features: - Current temperature - 7-day forecast - Severe weather alerts
Example: Spanish Store Listing
Title: Mi Extensión del Clima
Description: Obtén actualizaciones del clima en tiempo real para tu ubicación.
Características: - Temperatura actual - Pronóstico de 7 días - Alertas de clima severo
Best Practices for Store Listings
- Don’t just translate - Adapt content for cultural differences
- Keep titles under 45 characters to avoid truncation
- Use screenshots that show localized UI
- Test in multiple languages before publishing
Complete Example Project Structure
weather-extension/
├── _locales/
│ ├── en/messages.json
│ ├── es/messages.json
│ ├── fr/messages.json
│ ├── de/messages.json
│ ├── ja/messages.json
│ ├── ar/messages.json
│ └── zh_CN/messages.json
├── manifest.json
├── background.js
├── popup/
│ ├── popup.html
│ ├── popup.js
│ └── styles.css
├── images/
│ ├── icon-16.png
│ ├── icon-48.png
│ └── icon-128.png
└── README.md
manifest.json
{
"manifest_version": 3,
"name": "__MSG_extension_name__",
"description": "__MSG_extension_description__",
"version": "1.0.0",
"default_locale": "en",
"icons": {
"16": "images/icon-16.png",
"48": "images/icon-48.png",
"128": "images/icon-128.png"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "images/icon-16.png",
"48": "images/icon-48.png"
}
},
"background": {
"service_worker": "background.js"
},
"permissions": ["geolocation"]
}
Related Articles
- i18n API Reference - Complete reference for the chrome.i18n API
- Accessibility in Extensions - Building accessible extensions for all users
- Publishing to Chrome Web Store - Guide to publishing and managing your extension
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.