Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: i18n.getLanguageDictionary #274

Open
carlosjeurissen opened this issue Sep 15, 2022 · 22 comments
Open

Proposal: i18n.getLanguageDictionary #274

carlosjeurissen opened this issue Sep 15, 2022 · 22 comments
Assignees
Labels
i18n-tracker Group bringing to attention of Internationalization, or tracked by i18n but not needing response. proposal Proposal for a change or new feature supportive: chrome Supportive from Chrome supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari topic: localization

Comments

@carlosjeurissen
Copy link
Contributor

carlosjeurissen commented Sep 15, 2022

As requested by @Rob--W, splitting this proposal off from #258

Why

  • To be able to fetch messages from a different language than the browser UI language. This can for example be useful if you want to display your extension in a different language.
  • Fetching the language files manually is not really a doable alternative as it does not handle fallbacks to less specific language tags and English while adding a lot of boilerplate.

Proposal A

Introduce a i18n.getLanguageDictionary method. In which the first and only parameter is the language tag as string. Returning a promise with the languageDictionary allowing you to request messages from as follows:

browser.i18n.getLanguageDictionary('pt-BR').then((api) => {
  let someMessage = api.getMessage('some_id');
});

Or with async/await:

const brazilApi = await browser.i18n.getLanguageDictionary('pt-BR');
let someMessage = brazilApi.getMessage('some_id');

The name of this method can be worked on. Potential alternatives are createLanguageInstance and getLanguageInstance.

A variant on this is directly passing the getMessage method as suggested by @hanguokai in #274 (comment) .

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

Or with async/await:

const getPtBrMessage = await browser.i18n.getLanguageDictionary('pt-BR');
let someMessage = getPtBrMessage('some_id');

Proposal B

Introduce a i18n.getLanguageDictionary method. In which the first and only parameter is the language tag as string. Returning a promise with the languageDictionary allowing you to request messages from as follows:

browser.i18n.getLanguageDictionary('pt-BR').then((languageDictionary) => {
  let someMessage = browser.i18n.getMessage('some_id', substitutions?, {
    dictionary: languageDictionary,
  });
});

Proposal C

Introduce an asynci18n.initLanguage method. In which the first and only parameter is the language tag as string. This would initialise the messagedictionary on the background. The messages can then be fetched sync by passing the language parameter as option.

browser.i18n.initLanguage('pt-BR').then(() => {
  let someMessage = browser.i18n.getMessage('some_id', substitutions?, {
    language:'pt-BR',
  });
});

Proposal D

Allow passing the requested language synchronously. Most convenient for developers however potentially tricky implementation-wise.

  let someMessage = browser.i18n.getMessage('some_id', substitutions?, {
    language:'pt-BR',
  });

Proposal E

(from @hanguokai comment #274 (comment))

// set language to fr
await browser.i18n.setLanguage('fr');
browser.i18n.getMessage('hello'); // return France message: "Bonjour"

// set language to zh-CN
await browser.i18n.setLanguage('zh-CN');
browser.i18n.getMessage('hello'); // return  Simplified Chinese message: "你好"

This is similar to #258 . The difference is that #258 change the default language globally (all contexts) and persistently, but this proposal only change the language in the current context. For example, it doesn't affect another extension page.

Edge cases

  • If the passed language parameter is not of type string, it should throw an exception.
  • If the language tag can not be found, just like the normal getMessage method, it attempt to check for other available files. In the above example, it would first check pt-BR, then pt, then the default_locale (often english). Firefox skips the pt if the message is not present in pt-BR, yet is available in pt. We might want to raise a separate issue for this.
@carlosjeurissen carlosjeurissen added the proposal Proposal for a change or new feature label Sep 15, 2022
@xeenon
Copy link
Collaborator

xeenon commented Sep 15, 2022

What about getLocale or getLocaleMessages for this, to match the naming of the _locales directory and messages.json file?

@carlosjeurissen
Copy link
Contributor Author

@xeenon is true webExtensions currently mention "locale" here and there. This is something we can talk about as well.

From most use cases I have seen, the getMessage API in browser extensions is mostly used for language and not locale. The region part of the language tag is mostly used to make the language more specific vs for region purposes. Especially now we have the Intl web API, taking care of most regional formatting use cases.

This, and the fact most of the languages we currently handle in browserExtensions completely lack a regional suffix, would make me prefer language over locale not just for this API, but also in other areas of extensions.

@zombie zombie added the supportive: firefox Supportive from Firefox label Sep 29, 2022
@dotproto dotproto added follow-up: chrome Needs a response from a Chrome representative neutral: chrome Not opposed or supportive from Chrome and removed supportive: firefox Supportive from Firefox labels Sep 29, 2022
@Rob--W Rob--W added the neutral: firefox Not opposed or supportive from Firefox label Sep 29, 2022
@dotproto
Copy link
Member

dotproto commented Nov 9, 2022

In abstract, I think Chrome is supportive of an end user's desire to change an extension's language independent of the browser UI or an extension developer's desire to show strings in multiple languages. That said, we're a bit hesitant with the i18n.getLanguageDictionary() approach described in the original issue description.

As an alternative, what if we were to modify the signature of i18n.getMessage() to take a language property in an options object? (Note: Firefox does not currently support an options object.) The TypeScript signature for this method might look like this:

function getMessage(
    messageName: string,
    substitutions?: string|string[],
    options?: {
        escapeLt: boolean,
        // New! Takes a locale string such as "pt-BR"
        locale: string
    },
): Promise<string>

This approach may be more ergonomic for developers that wish to work with multiple languages while still allowing developers that wish to use the dictionary pattern to opt into it.

function getLocaleDictionary(locale: string) {
  return function(
      messageName: string,
      substitutions?: string|string[],
      options?: { escapeLt: boolean }
  ) {
    return browser.i18n.getMessage(messageName, substitutions, {...options, locale});
  }
}
let getPtBrMessage = getLocaleDictionary("pt-BR");
let message = getPtBrMessage("example_message");

@hanguokai
Copy link
Member

As an alternative, what if we were to modify the signature of i18n.getMessage() to take a language property in an options object?

@dotproto This is the original proposal of #258 (see edited history -> oldest). Then it evolves into a more perfect proposal, and still keep the api that can specify language parameter for getMessage.

@carlosjeurissen carlosjeurissen added agenda Discuss in future meetings topic: localization labels Nov 9, 2022
@xfq xfq added the i18n-tracker Group bringing to attention of Internationalization, or tracked by i18n but not needing response. label Mar 1, 2023
@dotproto dotproto removed the agenda Discuss in future meetings label Mar 16, 2023
@dotproto
Copy link
Member

At this point we appear to have broad alignment that this is a capability the platform should support. Next step is to align on an API design. We are open to proposals on how best to address this.

@carlosjeurissen carlosjeurissen self-assigned this Mar 18, 2024
@hanguokai
Copy link
Member

At this point we appear to have broad alignment that this is a capability the platform should support. Next step is to align on an API design. We are open to proposals on how best to address this.

@dotproto Do all browsers support #258 ? That is the same capability like this including i18n.getMessage with language code parameter.

@Rob--W
Copy link
Member

Rob--W commented Mar 19, 2024

At this point we appear to have broad alignment that this is a capability the platform should support. Next step is to align on an API design. We are open to proposals on how best to address this.

@dotproto Do all browsers support #258 ? That is the same capability like this including i18n.getMessage with language code parameter.

#258 differs from this issue, in that it requests changing the default locale and/or UI to change the default locale. This ticket is a request for a way to access the message strings without changing defaults, and the API is asynchronous by design. That can be implemented reasonably without a large implementation burden in browsers.

@hanguokai
Copy link
Member

This ticket is a request for a way to access the message strings without changing defaults

@Rob--W In #258 Main Content -> the 3rd section, i18n.getMessage(messageName, substitutions?, {language: "langCode"}). This part is not required to change the default locale.

@Rob--W
Copy link
Member

Rob--W commented Mar 19, 2024

This ticket is a request for a way to access the message strings without changing defaults

@Rob--W In #258 Main Content -> the 3rd section, i18n.getMessage(messageName, substitutions?, {language: "langCode"}). This part is not required to change the default locale.

The issue with this one is that the API is synchronous. A straightforward way to solve that part of the API is to introduce another async method that initializes the translation for the given locale.

@hanguokai
Copy link
Member

There was a feature request like this proposal, and Chrome was not sure whether to implement it. Because it is trivial for developers to implement this proposal. e.g.
let messages = await (await fetch(`/_locales/${locale}/messages.json`)).json();
Or use a full-featured JavaScript i18n library.


…… can be implemented reasonably without a large implementation burden in browsers.

Implementation burden can always be used as a reason for any proposal. But don't forget that platforms exist to serve developers and users, especially for common needs. Please balance real user needs and browser implementation burden and developer implementation burden.

@carlosjeurissen
Copy link
Contributor Author

carlosjeurissen commented Mar 20, 2024

let messages = await (await fetch(/_locales/${locale}/messages.json)).json();

It is not. Since this would not cover for substitutions and language fallbacks.

Implementation burden can always be used as a reason for any proposal. But don't forget that platforms exist to serve developers and users, especially for common needs. Please balance real user needs and browser implementation burden and developer implementation burden.

To make the API sync would mean the getMessage request would be blocking the main thread until the browser deals with preparing the langauge dictionary.

If I recall correctly devlin suggested another approach which roughly would look like:

browser.i18n.getLanguageDictionary('pt-BR').then((languageDictionary) => {
  let someMessage = browser.i18n.getMessage('some_id', substitutions?, {
    dictionary: languageDictionary,
  });
});

This seems pretty verbose. Another solution would be to merge the above with your (@hanguokai ) syntax. In which you first have to initialise the specific language, after which you can use it sync. So you get:

browser.i18n.initLanguage('pt-BR').then(() => {
  let someMessage = browser.i18n.getMessage('some_id', substitutions?, {
    language:'pt-BR',
  });
});

If the language has not been initialised yet, the getMessage call would result in an exception.

@hanguokai
Copy link
Member

It is not. Since this would not cover for substitutions and language fallbacks.

This part of the functionality can be encapsulated with a little extra code (I am currently using my own solution). Or use an independent I18N library as a solution as I said before. What I mean here is that this API can be easily implemented by developers themselves. Of course, it's always good if there is an official API.

sync vs async?

Why i18n.getMessage() is a sync API for the default locale. From the implementation perspective, when it is used for the first time, the browser reads the resource and caches it, so that there are no performance issues for subsequent uses.

#258 is to change the default locale, so it is still the sync way to use i18n.getMessage(). Because I think it is more in line with common use cases. Because users usually want the whole thing to be in one language, rather than having one part in one language and another part in another language. And I also support specifying a language parameter for some other use cases.

@carlosjeurissen
Copy link
Contributor Author

What I mean here is that this API can be easily implemented by developers themselves.

Even tho I disagree on this. With the same logic this would be true for 258 as well.

Because users usually want the whole thing to be in one language, rather than having one part in one language and another part in another language.

In the case of content scripts it makes a lot of sense to match the language of the page and thus control the language separate from the general extension language.

#258 is to change the default locale, so it is still the sync way to use i18n.getMessage()

Which would be great. I would love extension developers and potentially users to be able to modify the language used by an extension. I do not see how this proposal conflicts with #258 in any way. One does not exclude the other. As mentioned they cover different use cases.

@carlosjeurissen
Copy link
Contributor Author

@hanguokai @rdcronin added the alternative proposals to the main comment. Let me know your thoughts.

@hanguokai
Copy link
Member

With the same logic this would be true for 258 as well.

Of course, #258 can also be implemented by developers themselves, but there is more to do. You need to save the user's preferred language yourself, and read it first every time before getMessage(). As I said before, API design is a balance between browser implementation burden and developer implementation burden.

In the case of content scripts it makes a lot of sense to match the language of the page and thus control the language separate from the general extension language.

Maybe, but not necessarily. If I were a user, I would prefer it to be the language of the extension. For example, an extension that displays translated content of user selected-text on a page, its own UI should be the language of the extension.

I do not see how this proposal conflicts with #258 in any way. One does not exclude the other. As mentioned they cover different use cases.

They have some subtle intersections. But I agree that browsers can support both to cover different use cases.

@xeenon
Copy link
Collaborator

xeenon commented Mar 26, 2024

@rdcronin @carlosjeurissen devtools.panels.create() is an example API that returns a "live" API object, and not just a dictionary object. I think we should consider a similar model for this, so the extension can get an object (async via a Promise result) and use API methods on it instead of shoehorning this in some other way.

@hanguokai
Copy link
Member

@carlosjeurissen added the alternative proposals to the main comment. Let me know your thoughts.

Proposal A

The getLanguageDictionary() returns a object that has a getMessage() method. If getMessage() is the only method on it, maybe we can return a function object directly? for example:

const getFrMessage = await browser.i18n.getLanguageDictionary('fr');
getFrMessage('hello') // return France message: "Bonjour"

const getZhCNMessage = await browser.i18n.getLanguageDictionary('zh-CN');
getZhCNMessage('hello') // return  Simplified Chinese message: "你好"

Proposal B and C

They are similar. They both load a language first. It works. But:

  1. Developers don't know what languages already loaded.
  2. A bit verbose. Developers have to specify the language every time when calling getMessage().

Furthermore, I suggest it also accepts an array of languages, for example

// prepare one additional language
await browser.i18n.initLanguage('fr');

// prepare multiple additional languages
await browser.i18n.initLanguage(['fr', 'zh-CN']);

Proposal D

From a developer's perspective, this is the easiest to use. However, from an implementation perspective, browsers usually only cache a few languages: the default language (the "default_locale" in manifest file), the browser UI language and other fallback languages if possible. It is a bit difficult to have the browser load and cache all the languages.

I have another proposal: Proposal E

// set language to fr
await browser.i18n.setLangage('fr');
browser.i18n.getMessage('hello'); // return France message: "Bonjour"

// set language to zh-CN
await browser.i18n.setLangage('zh-CN');
browser.i18n.getMessage('hello'); // return  Simplified Chinese message: "你好"

This is similar to #258 . The difference is that #258 change the default language globally (all contexts) and persistently, but this proposal only change the language in the current context. For example, it doesn't affect another extension page.

@carlosjeurissen
Copy link
Contributor Author

@xeenon agreed returning a live API object is the most elegant of the options available especially when returning the getMessage function directly as suggested by @hanguokai.

@hanguokai Thanks for your feedback! Passing the method directly could be even more elegant indeed.

Incorporated this and Proposal E in the main issue.

As for accepting an Array, this could be done for any of the proposals. It reminds me of the yet unresolved discussion on how to deal with language fallbacks. See: #296

@rdcronin
Copy link
Contributor

I chatted with @oliverdunk about this. My current thinking:

  • I'm still opposed to returning a custom type (Proposal A). This is completely possible from an implementation standpoint, but it does significantly increase complexity and differ from the vast majority of other APIs. This is more challenging from an implementation perspective as well as a documentation perspective.
  • I'm okay with the idea of fetching a language dictionary and passing that into getMessage() (Proposal B). That solves the issue and allows for synchronous use of getMessage() while still allowing async fetching of the other language. I think I like this better than Proposal C (which is conceptually similar in the "init" phase) because passing in an object makes it more clear that there's an additional step that needs to happen (as opposed to passing in a language identifier only after initLanguage() was called).
  • Proposal D is the easiest for developers. I think there were concerns here around performance because getMessage() should be sync, and this may require fetching an additional language. However, I wonder if that's really a concern. The way getMessage() works today is that it lazily fetches a message bundle from the browser process on first access, and then that message bundle is available for all future calls. We could apply the same logic to fetching another language -- the first time a message of that language is fetched, the browser fetches that bundle. Theoretically, a developer could slow down the browser by callaing this for every known language, but I don't think that should be too much of a concern (extensions can already negatively impact browser performance). In the common case, we'd expect a developer to only request messages in a single language.
  • I think Proposal E makes sense, but has some limitations, as well -- it becomes challenging to request a single string in another language, since you'd have to set the language, call the API, and then set it back, and you'd have to ensure no other code in your extension set the string at a different point. I think Proposal E is potentially also beneficial, but I think there's value in allowing developers to fetch a non-primary language string.

With all these in mind, I think I'd most lean towards Proposal D. It seems the easiest for developers and most ergonomic, and I think browsers and implement it in a way to have reasonable performance.

@xeenon , WDYT?

@xeenon
Copy link
Collaborator

xeenon commented Jun 30, 2024

I would be happy with Proposal D and Proposal B (in that order).

@oliverdunk oliverdunk removed the follow-up: chrome Needs a response from a Chrome representative label Jul 1, 2024
@hanguokai
Copy link
Member

Proposal D was the exact same design I originally proposed in #258 and is an optional method in the final version of #258. The issue 274 was separated from the issue 258 because some people didn't accept it as a synchronous method at the time. As time goes by, if we finally decide to accept Proposal D, I can also add it as a supplement to #641.

@carlosjeurissen
Copy link
Contributor Author

This has been discussed during TPAC 2024 and it was agreed Proposal B is the preferred by all browsers. Proposal A was rejected as it introduces new concepts new to the extensions platform. Proposal D was rejected as it's synchronous nature requires blocking the main process which could be troublesome.

@carlosjeurissen carlosjeurissen added supportive: chrome Supportive from Chrome supportive: firefox Supportive from Firefox and removed neutral: chrome Not opposed or supportive from Chrome neutral: firefox Not opposed or supportive from Firefox labels Sep 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
i18n-tracker Group bringing to attention of Internationalization, or tracked by i18n but not needing response. proposal Proposal for a change or new feature supportive: chrome Supportive from Chrome supportive: firefox Supportive from Firefox supportive: safari Supportive from Safari topic: localization
Projects
None yet
Development

No branches or pull requests

9 participants