w3c / webextensions

Charter and administrivia for the WebExtensions Community Group (WECG)
Other
579 stars 50 forks source link

Per-extension language preferences #258

Open hanguokai opened 1 year ago

hanguokai commented 1 year ago

Update: In order to express it completely and clearly, I reorganized the proposal and edited it many times after 2022-09-12.

Summary

Currently, browser.i18n only display one default language to users. Users can't change the language to other extension supported languages independently. Developers need to use their own solutions to provide users with multiple language select menu.

It makes sense for an app to use another language independently of the operating system. For example, Android supports per-app language preferences (here is its docs and video). The same goes for extensions. This proposal brings per-app language preferences to browser extensions.

Tracking bugs: Chromium-1365283 , Firefox-1791356

Main Content

1. Browser built-in supply a language select menu per extension

Screen Shot 2022-09-04 at 14 30 17

Since this is a general purpose feature for all users and extensions, browser built-in support is best.

If browsers built-in support a language select menu per extensions, all developers, all existing extensions and users can get this benefit right away. Ideally, developers just use current i18n api without doing anything. Another benefit is that it's much easier for developers to test i18n.

2. New APIs for developers

/**
 * get the current language used by i18n.getMessage()
 * return the language tag,  e.g. en-US, zh-CN.
 */
i18n.getCurrentLanguage() => code: String.

/**
 * set a new language used in this extension.
 * if code is null, revert to the default state(follow the browser UI language).
 * if code is not valid, reject it.
 * return a Promise, resolved when the operation is complete.
 */
i18n.setCurrentLanguage(code: String) => Promise<void>

/**
 * get all languages that supported by this extensions.
 * return a Promise, resolved with an array of language tags.
 */
i18n.getAllLanguages() => code_array: Promise<Array<String>>

/**
 * After i18n changed to a new language, the browser triggers a language changed event.
 * callback is (new_language_code: String) => void
 */
i18n.onLanguageChanged.addListener(callback)

Ideally, developers just use current i18n api without doing anything if there is a browser-supplied language select menu. The new api is only used to integrate this feature with the developer-supplied language select menu. For example, in the extension's options page, developers use get/setCurrentLanguage and getAllLanguages to create a language select menu for users.

i18n.setCurrentLanguage(code) is persistent. It is a setting per extensions which saved by browsers. If the extension remove a language in the new version and current language is that, then browser fall back to the default language.

code is standard language code, like 'en-US', not 'en_US'(folder name).

How to get language display names? Use Intl.DisplayNames, for example:

const currentLanguage = i18n.getCurrentLanguage();
const displayName = new Intl.DisplayNames([currentLanguage], 
      { type: 'language' , languageDisplay: 'standard' });

const allLanguages = await i18n.getAllLanguages();
for (let code of allLanguages) {
    let name = displayName.of(code)); // language display name for code
    // use code and name to create a language select
}

After changing the language, the browser triggers a onLanguageChanged event. This event is useful for updating UI for already opened extension pages and other UI parts like badgeText, badgeTitle and context menu.

i18n.onLanguageChanged.addListener(function(newCode) {
    // update extension page
    button.textContent = i18n.getMessage(buttonLabel);

    // update extension UI parts
    action.setTitle({title: i18n.getMessage(title)});
    action.setBadgeText({text: i18n.getMessage(text)});
    contextMenus.update(...);
});

3. Another New API (optional for implementation)

i18n.getMessage(messageName, substitutions?,  {language: "langCode"})

At present, i18n.getMessage() doesn't allow specifying a different language. I suggest add a new property to specify a language in the options parameter(the 3rd parameter which already support a "escapeLt" property). Maybe it is useful for some developers or some use cases.

Related References

https://developer.chrome.com/docs/extensions/reference/i18n/ https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n https://crbug.com/660704

252

jcblw commented 1 year ago

I have had this similar issue with the i18n library. This caused use to have to write a custom solution that manually fetchs locales based on the user's selection. This would be a greatly welcomed feature in web extensions for my team and I.

xeenon commented 1 year ago

Cool idea!

hanguokai commented 1 year ago

Today, Google publishes a video on Android YouTube channel: Per-app language preferences. I recommend everyone to watch this video in its entirety, which is only 5 minutes. This video is my final purpose for this issue (just replace Android with Browser, and replace app with extension). So I changed the title to "Per-extension language preferences".

If browsers support browser.i18n.getMessage(a-specific-language), then developers can use this API to implement a language select menu for users (save user's preference in storage). Otherwise, developers have to write their own solution.

Further more, browsers can supply a built-in language menu for users, like below:

Screen Shot 2022-09-04 at 14 30 17

And supply a related api for developers to integrate this function in their language select menu in extension. For example: i18n.getCurrentLanguage() / i18n.setCurrentLanguage(), i18n.onCurrentLanguageChanged. In this way, user's language preference is saved by browser, not in storage by developers.

Juraj-Masiar commented 1 year ago

I like the idea of being able to call i18n.setCurrentLanguage and browser would then serve locals from a different file.

Comparing with browser.i18n.getMessage + supplying language code, where one would have to read the "user-selected" language code from somewhere, probably using some async method, which would make the whole call async or one would have to await some "language init" before any language related operations starts, which is a terrible idea.

hanguokai commented 1 year ago

I like the idea of being able to call i18n.setCurrentLanguage and browser would then serve locales from a different file.

Comparing with browser.i18n.getMessage + supplying language code, where one would have to read the "user-selected" language code from somewhere, probably using some async method, which would make the whole call async or one would have to await some "language init" before any language related operations starts, which is a terrible idea.

Yes, I implemented this solution before. First, async read user's language preference from browser.storage.local, then do other things. i18n.setCurrentLanguage(lang) is more easier than i18n.getMessage(lang_code). So I prefer i18n.setCurrentLanguage(lang) or completely let user use browser's extension-language-select UI setting. But i18n.getMessage(lang_code) may be useful for some developers or some use cases. If possible, hopefully both will be supported.

yankovichv commented 1 year ago

Yes, such a solution would be valuable.

xeenon commented 1 year ago

I also prefer the i18n.setCurrentLanguage() proposal too. Avoiding local storage each time a string is needed is good.

bershanskiy commented 1 year ago

The original comment mentions that "if the extension would like to supply a language selecting menu in the extension settings, it can't use i18n.getMessage() and has to use other workarounds." Out of curiosity, I wrote one such workaround and it boils down to loading the locale JSON from extension files via fetch or XmlHttpRequest, parsing it, and taking the required message:

const locale = 'en_US';
const messages = await (await fetch(chrome.runtime.getURL(`/_locales/${locale}/messages.json`))).json()
const message = messages[message_name];

This does not seem too bad and overall this feature is trivially polyfillable even currently. The only caveat is that this workaround would be async while i18n.setCurrentLanguage() is synchronous.

Also, turns out this exact idea was proposed back in 2016 (comment 5, code example it links to), so compatibility should be good.

I also prefer the i18n.setCurrentLanguage() proposal too. Avoiding local storage each time a string is needed is good.

Would this persist across browser restarts/extension reloads? What should happen when a locale is removed during the extension update?

Juraj-Masiar commented 1 year ago

@bershanskiy you've forgot to process the "placeholders" :). Also the workaround is still async, so all your code that would like to use translations now needs to await this.

The i18n.setCurrentLanguage should for sure be persisting. And removing lang would simply fallback to default lang, like it already does.

bershanskiy commented 1 year ago

@bershanskiy you've forgot to process the "placeholders" :).

Sure, this is basic POC example. A better polyfil would also check for actual existence of the file, would provide a list of available languages, etc. Also, there are a few other considerations:

  1. What language codes are to be used? E.g., en_US (like current folder name), en-us, en-US (like navigator.language), etc.
  2. Is there an equivalent for langaugechange event?

Also the workaround is still async, so all your code that would like to use translations now needs to await this.

Yes, it is. However, it seems like making i18n.getMessage sync and able to load strings from any locale would make it load a lot of useless data or be racy with i18n.setCurrentLanguage or might be needlessly blocking (while browser loads the file for the first time).

The i18n.setCurrentLanguage should for sure be persisting. And removing lang would simply fallback to default lang, like it already does.

That makes sense. But also it would make sense to make i18n.setCurrentLanguage async (like setBadgeText() and similar APIs) and make it throw exceptions on invalid languages or missing language files.

hanguokai commented 1 year ago

@bershanskiy As I said, the feature has workaround, but it is not trivial. This is why I made this proposal. I also explained it at comment-10 4 years ago. In my personal solution, I even move locale/messages.json into JS file, so it can be access in sync way. I have published 18 extensions, counting the extensions for self-use only, there are nearly 30 extensions. It's too cumbersome to use this i18n workaround for every extension. If browsers built-in support a language-select menu per extensions, all developers, existing extensions and users can get this benefit without doing anything. Another benefit is that it's much easier for developers to test i18n.

The other issues you mentioned, I think they are all specification/implementation details.

Here is an introduction to Android Per-app language preferences as reference.

Juraj-Masiar commented 1 year ago

Just to clarify, I originally imagined this to be a solution for the common use-case when user wants to change a language:

  1. extension would present list of languages
  2. user clicks one
  3. it would call the i18n.setCurrentLanguage (async, since it should store it somewhere)
  4. event could fire, or one could call runtime.reload() to make sure things are changed everywhere, (implementing a change event in all pages / modules could be too much)

Then when the extension re/starts, browser loads the selected language (or default if the selected doesn't exist anymore), fully async, blocking only the extension. I imagine it already works like this just the selected language doesn't come from the user preference.

But now that I'm thinking about it, all this could be done by the browser without any extension interaction. Something like the extension keyboard shortcuts, just less hidden :).

hanguokai commented 1 year ago

all this could be done by the browser without any extension interaction

Yes, just like the picture I drew at #issuecomment-1236272054. Developers just use current i18n api without doing anything else (except update already opened extension pages). The new api is only used to integrate it with the developer-self-supplied language select menu.

hanguokai commented 1 year ago

In order to express this proposal completely and clearly, I reorganized the content at the first post of this issue.

bershanskiy commented 1 year ago

One more nitpick about the proposal: should the language selection sync to user browser account (like storage.sync) or not (like storage.local)? Above discussion mentioned "local sotorage", but that could be accidental.

hanguokai commented 1 year ago

One more nitpick about the proposal: should the language selection sync to user browser account (like storage.sync) or not (like storage.local)?

Good question. Local or sync? I often encounter this problem in the development process. In other words, does the same person use different extension languages on different devices? My personal answer is: maybe (like dev/testers) , but most users would probably prefer sync. I think this is left to browsers to decide. There are some other similar settings in the browser, like the Pin status per extensions.

carlosjeurissen commented 1 year ago

Looks good @hanguokai! Some remarks:

Per-language syntax

Considering the browser needs to load one or more locale files to initialise i18n.getMessage, having an async initialiser method could be a solution to this. Something like the following:

i18n.createLanguageInstance('pt-BR').then((languageInstance) => {
    let someMessage = languageInstance.getMessage('some_id');
  });

@bershanskiy Using fetch is not really a doable alternative as it doesn't handle fallbacks to less specific language tags and English.

API names

To better express what each API does, some different set of method names seems to make sense: i18n.setCurrentLanguage --> i18n.setExtensionLanguage i18n.getCurrentLanguage --> i18n.getCurrentExtensionLanguage i18n.getAllLanguages --> i18n.getSupportedExtensionLanguages

If we do not want to lock the method names to extensions, we can rename Extension to Runtime.

Async

Considering the stand from Google on sync/async code: Extension APIs are almost always asynchronous. Anything that cannot be resolved directly in the renderer must be asynchronous, and even if something can be done in the renderer today, it should frequently be asynchronous to prevent future breakage if we move something to the browser process.

It makes sense to make i18n.setCurrentLanguage async as @bershanskiy proposed as it needs to set storage and having it async has no clear downside. Same goes for i18n.getAllLanguages.

Remove custom-set-language

As we already experienced issues with the behaviour of undefined (https://github.com/w3c/webextensions/issues/263), I suggest we clearly define the behaviour with i18n.setCurrentLanguage. First thought it is to accept null or a string, else throw a direct exception. If the string is not a supported or valid language tag, the returned promise can be rejected.

Custom language scope

It is not yet clear in what parts of the UI browsers should use the per-extension language. Should the language also be changed for other parts of the UI like:

Language tag

As for language tag syntax. An underscore (_) has been used for folder structures only while the the hyphen (-) is the standard. Other APIs like i18n.getUILanguage already return language tags with hyphens (-) so lets stick to that.

Language labels

How will browsers get the language labels for each language tag for i18n.getAllLanguages? In what language should the languages labels be? Should it be the browser UI language? The current extension language? Or be translated in its own language?

First thought is to translate them in their own language as this is most likely understandable by the user.

xeenon commented 1 year ago

Would this also need to change the languages returned by the web navigator APIs?

carlosjeurissen commented 1 year ago

@xeenon at the moment the navigator APIs are not dependent on the browser UI language. They are dependent on the acceptLanguages header / browser preference. So there seems no reason for the language of navigator APIs to be changed with this proposal.

hanguokai commented 1 year ago

@carlosjeurissen My replies is below.

Per-language syntax

I see this is moved to #274

API names

The name should follow existing naming conventions. In my option, browser.i18n namespace is completely around the internationalization for the extension itself, so I think there is no need to add "Extension" in the name.

Async

async is Ok for me.

custom-set-language

i18n.setCurrentLanguage is the core of these apis. The implementation should be consistent on all browsers. If the input is not correct, it should throw error. If developers use the tags returned by i18n.getAllLanguages, it should always be correct.

Custom language scope

I'm willing to listen to other people for these edge cases. My opinion is:

Language tag and label

Developers should use the tags and labels returned by i18n.getAllLanguages , that is [{ name: name_for_UI, code: tag_for_Value }, ……].

Would this also need to change the languages returned by the web navigator APIs?

IMO, navigator.language(s) should be unchanged.

carlosjeurissen commented 1 year ago

Thanks for your replies @hanguokai ! My replies are below.

Per-language syntax

I see this is moved to #274

Correct. We concluded in the meeting it makes sense to split the issue in half and put the getMessage proposal in a separate issue as this could be implemented separately and has a higher chance of being implemented short term. We can continue discussion in #274. Would love to hear your feedback.

The name should follow existing naming conventions. In my option, browser.i18n namespace is completely around the internationalization for the extension itself, so I think there is no need to add "Extension" in the name.

During the meeting today it seemed some members were confused about the intention of the method. And thought it would set the language of the whole browser, not just the extension scope. Considering this adding extension can add added clarity to what the method is doing.

i18n.setCurrentLanguage is the core of these apis. The implementation should be consistent on all browsers. If the input is not correct, it should throw error. If developers use the tags returned by i18n.getAllLanguages, it should always be correct.

My proposal was not to ditch i18n.setCurrentLanguage. It is indeed core to this set of APIs. My proposal here was to clearly define what should happen when passing null or undefined, or an empty string to setCurrentLanguage. When passing null, it for sure should fall back to the browser UI Language. That is why I added the header remove custom-set-language.

The browser might want to do some work in figuring out if the language tag being passed is valid. That is why I think it makes sense to reject the promise with an error stating the passed language tag is invalid or unavailable, versus directly throwing an exception when calling the method.

Custom language scope During the meeting we talked about some of the potential difficulties when it comes to some of these areas. For example, right clicking the extension icon shows not just native extension items but also items from the browser. Like "Remove extension". I'm not so sure we want them to be translated in a different language.

Now you mention action badge text and extension contextMenus, it seems to make most sense to keep them as is, and let it be the extension developers responsibility to update the labels if the extensions language changes.

Developers should use the tags and labels returned by i18n.getAllLanguages , that is [{ name: name_for_UI, code: tag_for_Value }, ……].

This would be good indeed. However, we should define what getAllLanguages should return and how it comes up with the names. That was what my original comment about language tag labels was about.

hanguokai commented 1 year ago

My proposal was not to ditch i18n.setCurrentLanguage. It is indeed core to this set of APIs. My proposal here was to clearly define what should happen when passing null or undefined, or an empty string to setCurrentLanguage. When passing null, it for sure should fall back to the browser UI Language. That is why I added the header remove custom-set-language.

The browser might want to do some work in figuring out if the language tag being passed is valid. That is why I think it makes sense to reject the promise with an error stating the passed language tag is invalid or unavailable, versus directly throwing an exception when calling the method.

I understand now, especially i18n.setCurrentLanguage(null) means reverting to the default state. I added it to the original post.

I agree with the rest.

bershanskiy commented 1 year ago

@hanguokai Could you please clarify the signature of the callback for i18n.onLanguageChanged event? I see two possibilities:

For now, I would propose to keep it simple and do just () => void.

hanguokai commented 1 year ago

Could you please clarify the signature of the callback for i18n.onLanguageChanged event?

@bershanskiy I think the callback is (new_lang_code) => void. Usually, developers can ignore the parameter, and update UI by i18n.getMessage, for example:

browser.i18n.onLanguageChanged.addListener(lang => {
    button.textContent = i18n.getMessage(key);
});

I don't know use cases for using reason.

carlosjeurissen commented 1 year ago

@bershanskiy It would indeed be good to know use cases for the knowing the reason of a language change.

We draw inspiration from the storage.onChanged event and return an object with newValue and potentially the oldValue. In which we could also add a potential reason property if we find any use cases for this.

So we end up with something like this:

browser.i18n.onExtensionLanguageChanged.addListener((changes) => {
  let { newValue, oldValue, reason } = changes;
});
hanguokai commented 1 year ago

I took a look at the current Chromium implementation of SharedL10nMap and GetI18nMessage.

SharedL10nMap says:

This class is thread-safe - it uses a lock to guard access to the underlying map whenever reading or modifying it. As such, no method directly returns the map or a reference to a value; instead, the map handles these operations (such as substitution) internally.

While a lock here seems alarming, in practice it's not as bad as it seems: the vast majority of the time, there will only be one thread accessing the map at a given moment (and it doesn't have to wait). The situation in which this doesn't hold is if a single extension has both a DOM-based context (like tab or popup) and an active service worker that each want to access the map at the same instant. This can happen, but should be rare enough that the lock isn't commonly a performance bottleneck.

SharedL10nMap sync gets locale resources and cache it, and is used as a singleton. In addition, i18n.getMessage() as a sync method is already supported in service worker now. So, I think:

In real-world usage scenarios, changing the language setting is triggered by the user and rarely happens. So it is not a performance issue.

bershanskiy commented 1 year ago

So we end up with something like this:

browser.i18n.onExtensionLanguageChanged.addListener((changes) => {
  let { newValue, oldValue, reason } = changes;
});

I feel like the most important API design decision is whether the listener takes a dictionary or an array of plain arguments. A dictionary is typically more forward-compatible because new attributes can be seamlessly added without needing to change the order of arguments. For MVP, probably the only truly necessary value is newValue. oldValue and reason are probably less useful.

hanguokai commented 1 year ago

For MVP, probably the only truly necessary value is newValue. oldValue and reason are probably less useful.

Agree. I updated the first post, added more details.

i18n.getCurrentLanguage() // return {name, code}
i18n.setCurrentLanguage(code)
i18n.getAllLanguages() // return [{name, code}, ......]
i18n.onLanguageChanged.addListener(callback) // callback is (newLang: {name, code}) => void

I think the intent and functionality of the API is pretty clear by now. The exact IDL and function name can be determined by browser vendors if they are interested in implementing it.

carlosjeurissen commented 1 year ago

@hanguokai What was your opinion on my proposed alternative naming? As for i18n.getCurrentLanguage(), to stay consistent with getUILanguage(), I suggest to just return the language tag as string. If the name is really needed, the i18n.getAllLanguages can be used. Same for the i18n.onLanguageChanged changes object.

hanguokai commented 1 year ago

As for i18n.getCurrentLanguage(), to stay consistent with getUILanguage(), I suggest to just return the language tag as string. If the name is really needed, the i18n.getAllLanguages can be used. Same for the i18n.onLanguageChanged changes object.

@carlosjeurissen Hi, let me explain my thoughts.

New Type: Language

Initially, I wanted to define a new type, that include language name for display.

class Language {
    name: String // for language display
    code: String // a language tag
}

In this way, the following APIs look consistent.

getCurrentLanguage() => language: Language,
getAllLanguages() => languages: Promise<Array<Language>>
onLanguageChanged(callback) // callback is (new_language: Language) => void

Only use standards language tags, no name

When you asked me again, I reconsidered about language names. Now, I propose another design: only use standards language tags.

getCurrentLanguage() => code: String
setCurrentLanguage(code: String) => Promise<void>
getAllLanguages() => code_array: Promise<Array<String>>
onLanguageChanged(callback) // callback is (new_language_code: String) => void

Now, how to get language names? My answer is using Intl.DisplayNames. For example:

const currentLanguage = i18n.getCurrentLanguage();
const allLanguages = await i18n.getAllLanguages();
const displayName = new Intl.DisplayNames([currentLanguage], 
      { type: 'language' , languageDisplay: 'standard' });

for (let code of allLanguages) {
    console.log(displayName.of(code)); // output display name for code
}

Language display names are a complex problem. Intl.DisplayNames is a ready-made mature solution, so don't solve this problem again in browser.i18n.

Note: Intl.DisplayNames accepts 'en-US' or 'EN-US', but it doesn't accept 'en_US'. See here.

hanguokai commented 1 year ago

What was your opinion on my proposed alternative naming?

Your proposed function name, of course it works, I just feel a little long-winded. I don't really care what the names are. I want to leave it to browser vendors to decide. After all, if they didn't implement it, it would be meaningless to call it anything.

hanguokai commented 1 year ago

I created two tracking issues for this feature: Chromium-1365283 and Firefox-1791356. I am not sure where to add for Safari.

carlosjeurissen commented 1 year ago

What was your opinion on my proposed alternative naming?

Your proposed function name, of course it works, I just feel a little long-winded. I don't really care what the names are. I want to leave it to browser vendors to decide. After all, if they didn't implement it, it would be meaningless to call it anything.

The point here is to find the optimal names, increasing the case for implementation. Suggestions by browser vendors is very welcome. As for feeling long-winded. We can change getCurrentExtensionLanguage to getExtensionLanguage to be shorter.

Language display names are a complex problem. Intl.DisplayNames is a ready-made mature solution, so don't solve this problem again in browser.i18n.

Note: Intl.DisplayNames accepts 'en-US' or 'EN-US', but it doesn't accept 'en_US'. See here.

Using Intl.DisplayNames is a great suggestion. Thanks for updating the initial post / proposal! Again the fact it does not support the lower dash (_) is not an issue. In extensions this syntax should only be used for _locales directories.

hanguokai commented 1 year ago

Reply some questions in 2022-09-29 meeting notes.

[tomislav] Would we need to provide a UI, or is an extension API sufficient? Note that implementing UI is a high bar, an API-only approach would be preferred.

@zombie If browser provide a UI, all extensions/users can get this feature without developers write one line of code. If browser only provide API, then only extensions that developers support it can get this feature. API support as the first step is also welcome.

[timothy] Curious whether there is user demand for this feature. Is this pretty common for bilingual people to change this?

@xeenon This feature is very common on the Web (not just for extensions). For example: In Gmail settings, users can change Gmail display language. Another example see this video.

[tomislav] Then I don't understand why this is per extension.

@zombie Sometimes the user just wants to change one extension's language, not all. Also, each extension supports different languages (e.g. Extension-A supports 2 languages, Extension-B supports 15 languages).

hanguokai commented 1 year ago

Reply another question in 2022-09-29 meeting notes.

[oliver] Would this also affect things like the extension name? [rob] One potential abuse vector for this api is if extensions dynamically change their own name without user action, and try to masquerade as some other “official” trusted brand app. [simeon] During TPAC I attended the web manifest session, where they discussed similar concerns about the web application being able to update metadata such as icons. I share the same concerns expressed there and by Rob.

@Rob--W @dotproto All names of an extension are defined in manifest.json by locale/messages.json, that are reviewed by the extension store. So, anyway, it can't changed to another extension's name. This api only switch languages, it can't change the messages.

Rob--W commented 1 year ago

About UI: The language selection logic is already quite complicated to users:

This proposal here asks about a way to allow extensions to support the content-level language settings as a first-class feature in extensions. The extension would still be responsible to offer the language selection logic.

If we were to introduce UI to allow users to customize the language for individual extensions, then there would be four places where a user can try to change a language. And then potentially be surprised that the UI-induced language change did not affect the extension and/or web pages. That's the disadvantage to choice: power users can customize, but some users would be confused.

Solving that is non-trivial.

hanguokai commented 1 year ago

This proposal here asks about a way to allow extensions to support the content-level language settings as a first-class feature in extensions. The extension would still be responsible to offer the language selection logic.

Exactly. This proposal let users switch extension languages like websites do.

If we were to introduce UI to allow users to customize the language for individual extensions, then there would be four places where a user can try to change a language. And then potentially be surprised that the UI-induced language change did not affect the extension and/or web pages.

When the user install the extension first time, the default behavior doesn't change (following the browser UI). When the user explicitly sets the extension language by themself, then the user should not be surprised later. This behavior is the same as websites do. For example, when a user visits a website(like MDN) first time, the website shows a default language. When user explicitly set a language by the website's language menu, then the website will remember this setting when the user visit it next time.

Note: users can change back to the default behavior(following the browser UI) by selecting the "default" item in the language menu.

Different websites support different languages, and so do extensions. The browser doesn't know what languages a website supports, but knows what languages an extension supports. Because extensions built-in have an i18n API. So the browser can't do this for websites, but can do this for extensions.

The specific UI design is up to the browser. I know adding a new UI part is not easy for browsers, so I agree that browsers can support the extension API first.

hanguokai commented 1 year ago

@zombie as a Firefox engineer said below in their tracking issue:

This could be useful for extension to itself allow the user to change the language, since we do automatic localization in CSS files and similar. We're unlikely to do this on our own, unless other browser vendors agree about a common API in the WECG issue. We're unlikely to be adding Firefox UI for this though.

@dotproto Hi Simeon, could you comment on Chrome's position on this API at here or Chromium-1365283? At least for the API part. The UI part can be considered later.

Rob--W commented 1 year ago

From today's meeting:

[jackie] I see Safari is supportive to it. But I don't know Chrome and Firefox's position. [oliver] I don’t know from Google’s perspective, need to follow up on it. Simeon, do you know anything from when you were with Google? [simeon] When I discussed it with the team, we had reservations with changing the strings the user sees in the extension page. We felt that there were sufficient tools to customize based on the user’s preferences.

stevenmason commented 2 months ago

Hi everyone, I was wondering if any decisions have been made regarding this? I have a chrome extension and users have asked for an option to switch language and this would be really helpful.

hanguokai commented 2 months ago

@stevenmason At present, no substantive progress. This is one of the things that I am dissatisfied with. Maybe they just don't have the human resources to do it.

In practice, developers currently have to abandon browser.i18n api to support this feature (i.e. use other methods or frameworks), or workaround it, or mix these things. And the purpose of this proposal is to make these simple.

stevenmason commented 2 months ago

Thank you for the prompt response. I guess it's not a high priority if there is a work around.

Even option 3 would be great and I assume it wouldn't be much work.

rdcronin commented 2 months ago

I think there are two pieces here.

Native Browser UI

From the Chrome side, I don't think this is something we'd pursue. I think this type of UI is best handled by the extension itself.

Allowing Other Languages in browser.i18n

This, I readily agree, is a pain point in the API today. It always falls back to the language the user has selected for the browser and, as is called out here and in issue #274 , this may not be desirable for the user. I do think we should do something about this. That's most similar to option 3 here and I think is further captured and expanded upon in #274.


I'm going to add the chrome-opposed label to this issue, because I don't think we're going to add UI for this in the near future. I am supportive of adding better API support for this, but that's separately tracked in #274.

hanguokai commented 2 months ago

@rdcronin My reply is as follows. I wish you'd take a little more time to think about it.

Goal

The goal of this proposal is to enhance the browser.i18n API to support implementing the user language selection menu. This is not the goal of #274 , or at least the author of #274 explicitly believes that #258 and #274 do not conflict.

History

  1. To achieve this goal, the original idea of this proposal was to support passing a language parameter in the i18n.getMessage() method, i.e. the option 3 in the current proposal.
  2. In the same year (2022) that I made this proposal, I found out that Android 13 introduced a new feature: Per-app language preferences. I think it is conceptually more complete than my original idea. The relationship between extensions and the browser is similar to the relationship between native apps and the operating system. I believe the Android system has carefully considered this feature.
  3. Furthermore, in addition to providing APIs for developers to provide their own UI, as a general function, it would be better if the browser built-in provides this UI. I'll explain this later.

After the above iteration, this is what this proposal looks like now.

Use Case

The real user need is that users select a preferred language in the settings and save it. After that, each UI of the extension is displayed in that language, like the popup page, content scripts, notifications and other extension pages.

API

To achieve this goal, and as a general feature, it is best to have the browser save this setting rather than having each extension save it itself. So the browser should provide APIs like get/setDefaultLocale() methods. This avoids the need for extensions have to asynchronously initialize language resources every time before they can use it.

If the browser does not provide this support, the extension code will become as follows:

let languageResource = null;
async function getLanguageResource() {
    if (!languageResource) {
        // read setting from storage, here I think storage.sync is better than storage.local
        let { lang } = await browser.storage.sync.get({ lang: "en_US" });
        languageResource = await initializeLanguageResource(lang);
    }
    return languageResource;
}

// in various places of building UI
async function buildX() {
    let div = document.createElement('div');
    let resource = await getLanguageResource();
    div.textContent = resource.getMessage('title');
}

async function buildY() {
    let button = document.createElement('button');
    let resource = await getLanguageResource();
    button.textContent = resource.getMessage('buttonName');
}

Note that this is just sample code, and concurrent initialization should be avoided in practice.

If the browser supports setDefaultLocale(), the code will be very simple:

// in various places of building UI
function buildX() {
    let div = document.createElement('div');
    div.textContent = browser.i18n.getMessage('title');
}

function buildY() {
    let button = document.createElement('button');
    button.textContent = browser.i18n.getMessage('buttonName');
}

In addition, this API is also beneficial for performance optimization. The browser usually cache the default language resource after first loading it. But for API in #274 , the browser doesn't know whether to cache the resource, because that API may dynamically load one or more language resources anytime, anywhere.

The Browser UI

First of all, the browser's built-in UI support for language selection is optional, and the API I mentioned earlier is more important.

But if the browser has built-in support for language selection, it has the following benefits:

In my initial idea, the user would right-click on the extension icon in the toolbar and then select the preferred language of the extension. Similar the following code by creating a context menu for the action.

for (let {langCode, langName} of allLanguages) {
    let context = {
      id: `lang-${langCode}`,
      title: langName,
      contexts: ["action"]
    };
    chrome.contextMenus.create(context);
}

It's a per-extension option, similar to other options that are presented and handled by the extension. Chrome has some of these, but the vast majority of these options are ones that should not or cannot be delegated to the extension (such as permission-related settings).

I18N is a general feature, not only for developers, but also for users. In this respect, the browser can play a bigger role.

The browser may not have perfect insight into which languages the extension supports. We can look at the messages file, but ...

Indeed. But the messages files provided by the extension means what languages the extension should support. Anyway, for extensions that already support multiple languages based on browser.i18n.getMessage(), these are the languages that they can be supported. To avoid the problem you mention, we can allow the extension to opt-in or opt-out this feature.

There doesn't seem to be significant benefit to having this be a browser-provided option over an extension-controlled UI.

For this proposal, the browser's built-in UI is optional, and I don't require the browser to must support it.

I think having the extension control it is more inline with other similar models, such as selecting your language for a particular site.

Websites and extensions are different. Websites don't have a unified API to support i18n, so browsers can't provide a unified UI for websites, but extensions can.

hanguokai commented 2 months ago

I'm going to add the chrome-opposed label to this issue, because I don't think we're going to add UI for this in the near future. I am supportive of adding better API support for this

Because the browser UI part is optional for this proposal, I removed the "chrome-opposed" label and added the "follow-up" label for further discussion.

rdcronin commented 2 months ago

Thank you for the detailed response, @hanguokai !

I'm still not entirely on board that there's a major effective difference between the two versions of code to create the HTML that were included -- in all likelihood, we'd recommend any developers that wanted to support that to just write a wrapper around i18n like getMessageInPreferredLanguage(), which abstracts out the getLanguageResource(). There would be a difference between one version being sync (ish, due to browser implementation details) and another being async, but I think in the majority of cases that developers are dynamically generating HTML like that, it would depend on other async data (e.g. retrieving other settings, retrieving user data, etc), so the difference becomes less impactful. The browser caching is an interesting point, but is also very dependent on browser implementation details, and I'd be hesitant to design (or introduce) APIs based on that behavior if we think it may change.

However, when discussing this more with Oliver, I did remember another part of this that I don't think has been brought up here. In addition to setting the value returned by chrome.i18n.getMessage() (which could be worked around by allowing the extension to get a message from another language), we also allow embedding localized messages in other resources, such as CSS files. In Chrome, these are rewritten on-the-fly as they're fetched from the extension, using the user's current preferred locale. If the extension wanted to replicate this behavior with custom language preferences, they would need to dynamically generate these resources when they otherwise wouldn't. While I don't think the additional wrapping in JS adds significant complexity, I do think that requiring styles to be dynamically generated through JS instead of packaged as CSS is a significant burden.

I think that's enough to convince me that this is worthwhile doing -- I agree the other use cases are valuable and it's a nice-to-have for extension developers, and combined with that, I think there's sufficient justification for this.

I'm still opposed to introducing browser UI for this at this time. While I understand the utility and desire behind it, I think we should hold off on anything that requires standardizing UI between different browsers.

With this, I think the set of API methods we would need would be get/setDefaultLocale() (or CurrentLanguage as mentioned in the original comment), allowing an extension to set the default it wants the browser to use for that extension. For that subset of functionality, I'm supportive on the Chrome side.

hanguokai commented 1 month ago

Thank you for discussing this issue during the meeting(2024/05/09). This proposal consists of two parts: the API part and the Browser UI part. Now, all browsers are supportive of the API part. I will add corresponding labels.

Next, we hope to see a formal proposal that includes the behavior of the API and some details. I plan to write a formal proposal in the coming weeks. We can discuss the details in the proposal.

xPaw commented 3 weeks ago

If getCurrentLanguage is added, its format will be supported by Intl APIs, so we can use the various formatters in correct language Intl.NumberFormat(browser.i18n.getCurrentLanguage()) as opposed to Intl.NumberFormat(undefined).

i18n.getMessage variant that accepts a language code would also be good to have, when you want to force something to be in a different language (e.g. to match strings to be in the page's language for extensions that add elements to pages).

I agree browser UI implementation for changing languages should not block the implementation of the API (there's nothing preventing an UI to be created for this later on)

carlosjeurissen commented 3 weeks ago

@xPaw currently the extension language will always match the browser language which can be fetched using i18n.getUILanuage. Thus you can already pass this language to the Intl formatters.

hanguokai commented 3 weeks ago

If getCurrentLanguage is added, its format will be supported by Intl APIs

I think yes, because both this api and Intl api use a string with a BCP 47 language tag. If the developer never called i18n.setCurrentLanguage(lang), i18n.getCurrentLanguage() returning undefined or the current language that used by getMessage() is to be discussed. I will write a proposal later.

currently the extension language will always match the browser language

I think this is not true. For example, the language of the browser UI is English,

  1. if the extension only supports Chinese (so getMessage() always return Chinese), the extension is displayed in Chinese.
  2. if the extension supports French(default) and Japanese, the extension is displayed in French.
  3. if the extension setCurrentLanguage(lang) to Spanish, the extension is displayed in Spanish even though it supports English.