23

I found a very easy way to implement translation (or localization) of my Google Chrome Extension, but that seems to apply only to .json, css and js files.

But how to localize my html content, say in the popup or an options window?

c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • 2
    You need to use custom i18n libraries, since i18n is not natively supported for HTML: https://code.google.com/p/chromium/issues/detail?id=115800 – Rob W Aug 24 '14 at 09:11
  • @RobW: While searching the web I came across [this HTML solution](http://tumble.jeremyhubert.com/post/7076881720/translating-html-in-a-chrome-extension) Is that what you mean? – c00000fd Aug 24 '14 at 09:21
  • 1
    @c00000fd That would be the simplest and most inflexible implementation. Though unless you intend to use HTML in the strings, I suggest to use `.textContent` over `.innerHTML`. I was actually thinking of logic along the lines of [webL10n](https://github.com/fabi1cazenave/webL10n). The localization files are inspired by the formats used in Firefox, but the logic could more generally be applied to Chrome extensions as well (and nothing prevents you from using a custom i18n solution over `chrome.i18n`). – Rob W Aug 24 '14 at 09:29

12 Answers12

40

What you would do is this.

First, in your HTML use the same syntax as Chrome requires anywhere else. So your basic popup.html will be:

<!DOCTYPE html>
<html>
<head>
<title>__MSG_app_title__</title>
</head>
<body>

<a href="http://example.com/" title="__MSG_prompt001__">__MSG_link001__</a>

<!-- Need to call our JS to do the localization -->
<script src="popup.js"></script>
</body>
</html>

Then provide the usual translation in _locales\en\messages.json:

{
    "app_title": {
        "message": "MyApp",
        "description": "Name of the extension"
    },
    "link001": {
        "message": "My link",
        "description": "Link name for the page"
    },
    "prompt001": {
        "message": "Click this link",
        "description": "User prompt for the link"
    }
}

And finally your popup.js will perform the actual localization:

function localizeHtmlPage()
{
    //Localize by replacing __MSG_***__ meta tags
    var objects = document.getElementsByTagName('html');
    for (var j = 0; j < objects.length; j++)
    {
        var obj = objects[j];

        var valStrH = obj.innerHTML.toString();
        var valNewH = valStrH.replace(/__MSG_(\w+)__/g, function(match, v1)
        {
            return v1 ? chrome.i18n.getMessage(v1) : "";
        });

        if(valNewH != valStrH)
        {
            obj.innerHTML = valNewH;
        }
    }
}

localizeHtmlPage();
ahmd0
  • 16,633
  • 33
  • 137
  • 233
  • 4
    +1 for this simple solution, works for me, BUT WARNING for others, this function might replace whole html, and then all js data like handlers and nodes stored in variables are reset, so better to call this function in the very beggining before any script – Jacek Rosłan Mar 07 '19 at 14:34
  • Replacing html wastes time on serializing/reparsing it again so it's best to [use the built-in TreeWalker API](https://stackoverflow.com/a/7275856). – wOxxOm Oct 13 '21 at 14:08
14

Plain an simple:


{
  "exmaple_key": {
    "message": "example_translation"
  }
}


<sometag data-locale="example_key">fallback text</sometag>


document.querySelectorAll('[data-locale]').forEach(elem => {
  elem.innerText = chrome.i18n.getMessage(elem.dataset.locale)
})

pykiss
  • 949
  • 12
  • 15
  • 2
    This works and seems the simplest to implement. +1 from me. – darbid Jul 17 '20 at 14:15
  • A little tips chrome.i18n.getMessage is not case sensitive. chrome.i18n.getMessage('AAA') and chrome.i18n.getMessage('aaa') are the same. – weiya ou Nov 11 '21 at 03:41
8

Building from ahmd0's answer. Use a data attribute to allow a hard-coded fallback.

<!DOCTYPE html>
<html>
    <head>
        <title data-localize="__MSG_app_title__">My Default Title</title>
    </head>
    <body>
        <a href="http://example.com/" title="__MSG_prompt001__" data-localize="__MSG_link001__">Default link text</a>

        <script src="localize.js"></script>
    </body>
</html>

Then provide the usual translation in _locales\en\messages.json:

{
    "app_title": {
        "message": "MyApp",
        "description": "Name of the extension"
    },
    "link001": {
        "message": "My link",
        "description": "Link name for the page"
    },
    "prompt001": {
        "message": "Click this link",
        "description": "User prompt for the link"
    }
}

And finally your localize.js will perform the actual localization:

function replace_i18n(obj, tag) {
    var msg = tag.replace(/__MSG_(\w+)__/g, function(match, v1) {
        return v1 ? chrome.i18n.getMessage(v1) : '';
    });

    if(msg != tag) obj.innerHTML = msg;
}

function localizeHtmlPage() {
    // Localize using __MSG_***__ data tags
    var data = document.querySelectorAll('[data-localize]');

    for (var i in data) if (data.hasOwnProperty(i)) {
        var obj = data[i];
        var tag = obj.getAttribute('data-localize').toString();

        replace_i18n(obj, tag);
    }

    // Localize everything else by replacing all __MSG_***__ tags
    var page = document.getElementsByTagName('html');

    for (var j = 0; j < page.length; j++) {
        var obj = page[j];
        var tag = obj.innerHTML.toString();

        replace_i18n(obj, tag);
    }
}

localizeHtmlPage();

The hard-coded fallback avoids the i18n tags being visible while the JavaScript does the replacements. Hard-coding seems to negate the idea of internationalisation, but until Chrome supports i18n use directly in HTML we need to use JavaScript.

PlasmaDan
  • 475
  • 8
  • 14
6

As RobW noted in a comment, a feature request for adding i18n support in HTML using the same mechanism was created, but it has since then been rejected due to performance and security concerns. Therefore you can't use the same approach.

The issue mentions one possible workaround: to have separate HTML pages per language and switch between them in the manifest:

  "browser_action": {
    "default_popup": "__MSG_browser_action_page__"
  }

But if that's not a suitable approach, the only way is to translate the page dynamically via JavaScript. You mention a solution the simplest approach, by just tagging elements to translate with ids and replacing them on page load.

You can also employ more sophisticated tools like webL10n in parallel with Chrome's approach. Note that you should probably still minimally implement Chrome's approach, so that Web Store knows that the item is supporting several languages.

Pyves
  • 6,333
  • 7
  • 41
  • 59
Xan
  • 74,770
  • 16
  • 179
  • 206
  • Note: most of the information is due to RobW's comments, and as such marking this as a community wiki. – Xan Aug 24 '14 at 10:39
  • Thanks. And yes, I was basically asking if there's a special sequence of characters (like `__MSG_*__`, btw, you forgot two last underscores in your example) that one can also use in HTML markup. JavaScript solution works too. I was just looking for an easier implementation. But since there's a feature request out for HTML-only solution, they might implement it in the future. Good. – c00000fd Aug 24 '14 at 19:19
  • Technically, it was not _my_ example. You're welcome. – Xan Aug 24 '14 at 19:20
  • A follow up. I just came across an interesting find. If you open the Chrome settings window, then right click it and select "view source" you'll see that it's laid out using tags as such `

    `. Could that `i18n-content` attribute be for localization?
    – c00000fd Aug 27 '14 at 04:58
  • 2
    Can anyone confirm this is not working anymore? I tried it but got "file not found" error page instead – Ryan Nov 14 '17 at 16:37
1

Rather than parsing the full DOM, just add a class "localize" to the elements that have to be translated and add a data attribute data-localize="open_dashboard"

<div class="localize" data-localize="open_dashboard" >
  Open Dashboard
</div>

JavaScript :

$('.localize').each(function(index,item){
    var localizeKey = $(item).data( 'localize' );
    $(item).html(chrome.i18n.getMessage(localizeKey));
});

'_locales/en/messages.json' file

{
    "open_dashboard": {
        "message": "Open Dashboard",
        "description": "Opens the app dashboard"
    }
}
k441i
  • 31
  • 4
1

A workaround to avoid replacements:

Use a simple "redirect"

It works for popups and options

  1. In your manifest, declare the default popup

    "default_popup": "popup/redirect.html"
    
  2. The popup/redirect.html is almost empty. It just includes the script link to the redirect script

        <!DOCTYPE html>
        <html>
    <head></head>
    <body>
        <script src="redirect.js"></script>
    </body>
    

  3. The popup/redirect.js file is very simple too:

    var currentlang = chrome.i18n.getMessage("lang");
    var popupUrl = chrome.runtime.getURL("popup/popup-"+currentlang+".html");
    window.location.href = popupUrl;
    
  4. Create multiple popups, already localized:

    • popup-fr.html
    • popup-en.html
  5. Go into each of your messages.json files (in _locales) and add a "lang" message with the current language abbreviation as value: en for the english json, fr in the french json...

    • example for _locales/en/message.json:

       "lang": {
       "message": "en",
       "description": "Locale language of the extension."
       },
      

A simple workaround for very small project... definitely not a good choice for large ones. And it also works for Option pages.

Fxp
  • 115
  • 8
0

One of the ways to localize your content in popup html is to fetch it from javascript onLoad. Store the strings in the _locales folder under various languages supported by you as mentioned here and do chrome.i18n.getMessage("messagename") to fetch and load the variable strings and set them using javascript/jquery onLoad function for each html element from your background.js or whatever js you load before your html pages loads.

Lavixu
  • 1,348
  • 15
  • 20
0

I faced the same problem, but I solved it with a simple approach using custom data attributes.

Implement a localizing class that uses chrome.i18n and call it in the DOMContentLoaded event. In HTML, mark up the element you want to localize with the data-chrome-i18n attribute. (This attribute name is tentatively named.) Specifying the message name as the value of this attribute localizes the text content of the element. If you want to localize an attribute, specify it in the format attribute_name=message_name. Multiple specifications can be specified by separating them with ;.

const i18n = (window.browser || window.chrome || {}).i18n || { getMessage: () => undefined };

class Localizer {
  constructor(options = {}) {
    const { translate = Localizer.defaultTranslate, attributeName = Localizer.defaultAttributeName, parse = Localizer.defaultParse } = options;
    this.translate = translate;
    this.attributeName = attributeName;
    this.parse = parse;
  }
  localizeElement(element) {
    for (const [destination, name] of this.parse(element.getAttribute(this.attributeName))) {
      if (!name)
        continue;
      const message = this.translate(name) || '';
      if (!destination) {
        element.textContent = message;
      }
      else {
        element.setAttribute(destination, message);
      }
    }
  }
  localize(target = window.document) {
    const nodes = target instanceof NodeList ? target : target.querySelectorAll(`[${CSS.escape(this.attributeName)}]`);
    for (const node of nodes)
      this.localizeElement(node);
  }
}
Localizer.defaultTranslate = i18n.getMessage;
Localizer.defaultAttributeName = 'data-chrome-i18n';
Localizer.defaultParse = (value) => {
  return (value || '').split(';').map(text => (text.includes('=') ? text.split('=') : ['', text]));
};

const localizer = new Localizer();
window.addEventListener('DOMContentLoaded', () => {
  localizer.localize();
});
<!DOCTYPE html>
<html data-chrome-i18n="lang=@@ui_locale">

<head>
  <meta charset="UTF-8" />
  <title data-chrome-i18n="extensionName"></title>
</head>

<body>
  <p data-chrome-i18n="foo;title=bar;lang=@@ui_locale"></p>
</body>

</html>

There are several things to consider to solve this problem.

  • Use chrome.i18n (Many people will want to aggregate in messages.json.)
  • Supports attributes as well as element content
  • Supports not only popup but also options page
  • Rendering performance
  • Security

First, the approach of switching HTML for each language in manifest.json does not work. Even if you give __MSG_*__ to the default_popup field, popup will still show the error "ERR_FILE_NOT_FOUND". I don't know why. There is no detailed reference to default_popup in the Chrome extensions Developer Guide, but MDN mentions that it is a localizable property. Similarly, if you give __MSG _*__ to the page field in options_ui, the extension itself will fail to load.

I intuitively felt that the approach of replacing __MSG_*__ in HTML and rewriting the result usinginnerHTML had performance and security problems.

0

This answer is cool!

And I want to make some modifications.

For chrome 93.0.4577.63 chrome.i18n.getMessage permalink, link-by-version

chrome.i18n.getMessage(messageName, substitutions, {escapeLt})

So I want to make it support

  • substitutions
  • escapeLt

Test Data

// _locales/en/messages.json
{
  "hello": {    
    "message": "<b>Hello</b> $USER$ Welcoming $OUR_SITE$. $EMOJI$",
    "description": "Greet the user",
    "placeholders": {
      "user": {
        "content": "$1", // chrome.i18n.getMessage("hello", "variable 1")
        "example": "Carson"
      },
      "our_site": {
        "content": "Example.com"
      },
      "emoji": {
        "content": "$2",
        "example": "\uD83D\uDE42" // , 
      }
    }
  },
  "app": {
    "message": "My cool APP.",
    "description": "description"
  }
}
<!-- test.html-->

<script src="my-i18n.js"></script>

<p data-i18n="__MSG_hello__"></p>

<p data-i18n="__MSG_hello__<b>Carson</b>"></p>
<p data-i18n="__MSG_hello__<b>Carson</b>|0"></p>
<p data-i18n="__MSG_hello__<i>Carson</i>|1"></p>

<button title="__MSG_hello__<b>Carson</b>" data-i18n></button>
<button title="__MSG_hello__<b>Carson</b>|0" data-i18n></button>
<button title="__MSG_hello__<b>Carson</b>|1" data-i18n></button>

<p title="__MSG_app__" data-i18n="__MSG_hello__Carson,"></p>

output

enter image description here

Script

// my-i18n.js


/**
 * @param {string} msg "__MSG_Hello__para1,para2|1"  or "__MSG_Hello__para1,para2|0"
 * */
function convertMsgAsFuncPara(msg) {
  const match = /__MSG_(?<id>\w+)__(?<para>[^|]*)?(\|(?<escapeLt>[01]{1}))?/g.exec(msg) // https://regex101.com/r/OeXezc/1/
  if (match) {
    let {groups: {id, para, escapeLt}} = match
    para = para ?? ""
    escapeLt = escapeLt ?? false
    return [id, para.split(","), Boolean(Number(escapeLt))]
  }
  return [undefined]
}

function InitI18nNode() {
  const msgNodeArray = document.querySelectorAll(`[data-i18n]`)
  msgNodeArray.forEach(msgNode => {
    const [id, paraArray, escapeLt] = convertMsgAsFuncPara(msgNode.getAttribute("data-i18n"))
    if (id) {
      msgNode.innerHTML = chrome.i18n.getMessage(id, paraArray, {escapeLt})
    }

    // ↓ handle attr
    for (const attr of msgNode.attributes) {
      const [attrName, attrValue] = [attr.nodeName, attr.nodeValue]
      const [id, paraArray, escapeLt] = convertMsgAsFuncPara(attrValue)
      if (!id) {
        continue
      }
      msgNode.setAttribute(attrName, chrome.i18n.getMessage(id, paraArray, {escapeLt}))
    }
  })
}

(() => {
  window.addEventListener("load", InitI18nNode, {once: true})
})()
Carson
  • 6,105
  • 2
  • 37
  • 45
0

Modify pseudo-category content in batches.

 <div data-content="font"></div>
div::before {
  content: attr(data-content);
}
document.querySelectorAll('[data-content]').forEach(el => {
  el.dataset.content = chrome.i18n.getMessage(el.dataset.content);
});

Use CSS Internationalization.

<p></p>
p::before {
  content: "__MSG_font__";
}
weiya ou
  • 2,730
  • 1
  • 16
  • 24
-1

Another workaround - you can use content property in css with __MSG_myText inside.

Vitaly Zdanevich
  • 13,032
  • 8
  • 47
  • 81
-1

Use Vue.js:

<html>
  <head>
  </head>
  <body>
    <div id="app">{{msgTranslated}}</div>
  </body>
</html>

javascript file injected:

new Vue({
  el: '#app',
  data: {
    msgTranslated: chrome.i18n.getMessage("message")
  }
})
Andre Lopes
  • 99
  • 1
  • 4