Open hanguokai opened 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.
Cool idea!
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:
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.
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.
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 someasync
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.
Yes, such a solution would be valuable.
I also prefer the i18n.setCurrentLanguage()
proposal too. Avoiding local storage each time a string is needed is good.
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?
@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 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:
en_US
(like current folder name), en-us
, en-US
(like navigator.language
), etc.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.
@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.
Just to clarify, I originally imagined this to be a solution for the common use-case when user wants to change a language:
i18n.setCurrentLanguage
(async, since it should store it somewhere)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 :).
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.
In order to express this proposal completely and clearly, I reorganized the content at the first post of this issue.
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.
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.
Looks good @hanguokai! Some remarks:
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.
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.
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
.
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.
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:
options_ui
settings pagesaction
badge textcontextMenus
Window.alert
)__MSG_messagename__
syntax.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.
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.
Would this also need to change the languages returned by the web navigator
APIs?
@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.
@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:
setBadgeText
by i18n.getMessage
contextMenus.create
, which can specify the title by i18n.getMessage
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.
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 byi18n.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.
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.
@hanguokai Could you please clarify the signature of the callback
for i18n.onLanguageChanged
event? I see two possibilities:
() => void
(reason) => void
or ({reason}) => void
where reason
is an enum explaining why the change occurred. What are the possible reasons then? I imagine the following reasons: user changing global preferences, user changing extension-specific preferences, browser synching global preferences, browser synching extension-specific preferences, extension removing a locale.For now, I would propose to keep it simple and do just () => void
.
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
.
@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;
});
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:
i18n.getCurrentLanguage()
is a sync method, return a cache of current language.i18n.setCurrentLanguage(lang)
is a async method that will cause SharedL10nMap reload locale resource and cache it. After reloading locale resource, it triggers an onLanguageChanged
event.i18n.getAllLanguages()
is a async method, browsers don't have to cache it.In real-world usage scenarios, changing the language setting is triggered by the user and rarely happens. So it is not a performance issue.
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.
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.
@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.
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.
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
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.
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.
I created two tracking issues for this feature: Chromium-1365283 and Firefox-1791356. I am not sure where to add for Safari.
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.
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).
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.
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.
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.
@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.
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.
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.
@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.
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.
I think there are two pieces here.
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.
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.
@rdcronin My reply is as follows. I wish you'd take a little more time to think about it.
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.
i18n.getMessage()
method, i.e. the option 3 in the current proposal.After the above iteration, this is what this proposal looks like now.
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.
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.
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.
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.
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.
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.
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)
@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.
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,
getMessage()
always return Chinese), the extension is displayed in Chinese.setCurrentLanguage(lang)
to Spanish, the extension is displayed in Spanish even though it supports English.
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
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
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
andgetAllLanguages
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:
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.3. Another New API (optional for implementation)
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