Skip to content

Commit

Permalink
feat: add app.getPreferredSystemLanguages() API (#36290)
Browse files Browse the repository at this point in the history
feat: add app.getPreferredSystemLanguages() API (#36035)

* feat: add app.getSystemLanguage() API

* Change the API to getPreferredSystemLanguages

* Fix test

* Clarify docs and add Linux impl

* Remove USE_GLIB

* Don't add C to list

* Remove examples since there's a lot of edge cases

* Fix lint

* Add examples

* Fix compile error

* Apply PR feedback

* Update the example

Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
  • Loading branch information
trop[bot] and rzhao271 committed Nov 9, 2022
1 parent 3026615 commit d8a1298
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 9 deletions.
36 changes: 35 additions & 1 deletion docs/api/app.md
Expand Up @@ -717,6 +717,8 @@ To set the locale, you'll want to use a command line switch at app startup, whic

**Note:** This API must be called after the `ready` event is emitted.

**Note:** To see example return values of this API compared to other locale and language APIs, see [`app.getPreferredSystemLanguages()`](#appgetpreferredsystemlanguages).

### `app.getLocaleCountryCode()`

Returns `string` - User operating system's locale two-letter [ISO 3166](https://www.iso.org/iso-3166-country-codes.html) country code. The value is taken from native OS APIs.
Expand All @@ -725,10 +727,42 @@ Returns `string` - User operating system's locale two-letter [ISO 3166](https://

### `app.getSystemLocale()`

Returns `string` - The current system locale. On Windows and Linux, it is fetched using Chromium's `i18n` library. On macOS, the `NSLocale` object is used instead.
Returns `string` - The current system locale. On Windows and Linux, it is fetched using Chromium's `i18n` library. On macOS, `[NSLocale currentLocale]` is used instead. To get the user's current system language, which is not always the same as the locale, it is better to use [`app.getPreferredSystemLanguages()`](#appgetpreferredsystemlanguages).

Different operating systems also use the regional data differently:

* Windows 11 uses the regional format for numbers, dates, and times.
* macOS Monterey uses the region for formatting numbers, dates, times, and for selecting the currency symbol to use.

Therefore, this API can be used for purposes such as choosing a format for rendering dates and times in a calendar app, especially when the developer wants the format to be consistent with the OS.

**Note:** This API must be called after the `ready` event is emitted.

**Note:** To see example return values of this API compared to other locale and language APIs, see [`app.getPreferredSystemLanguages()`](#appgetpreferredsystemlanguages).

### `app.getPreferredSystemLanguages()`

Returns `string[]` - The user's preferred system languages from most preferred to least preferred, including the country codes if applicable. A user can modify and add to this list on Windows or macOS through the Language and Region settings.

The API uses `GlobalizationPreferences` (with a fallback to `GetSystemPreferredUILanguages`) on Windows, `\[NSLocale preferredLanguages\]` on macOS, and `g_get_language_names` on Linux.

This API can be used for purposes such as deciding what language to present the application in.

Here are some examples of return values of the various language and locale APIs with different configurations:

* For Windows, where the application locale is German, the regional format is Finnish (Finland), and the preferred system languages from most to least preferred are French (Canada), English (US), Simplified Chinese (China), Finnish, and Spanish (Latin America):
* `app.getLocale()` returns `'de'`
* `app.getSystemLocale()` returns `'fi-FI'`
* `app.getPreferredSystemLanguages()` returns `['fr-CA', 'en-US', 'zh-Hans-CN', 'fi', 'es-419']`
* On macOS, where the application locale is German, the region is Finland, and the preferred system languages from most to least preferred are French (Canada), English (US), Simplified Chinese, and Spanish (Latin America):
* `app.getLocale()` returns `'de'`
* `app.getSystemLocale()` returns `'fr-FI'`
* `app.getPreferredSystemLanguages()` returns `['fr-CA', 'en-US', 'zh-Hans-FI', 'es-419']`

Both the available languages and regions and the possible return values differ between the two operating systems.

As can be seen with the example above, on Windows, it is possible that a preferred system language has no country code, and that one of the preferred system languages corresponds with the language used for the regional format. On macOS, the region serves more as a default country code: the user doesn't need to have Finnish as a preferred language to use Finland as the region,and the country code `FI` is used as the country code for preferred system languages that do not have associated countries in the language name.

### `app.addRecentDocument(path)` _macOS_ _Windows_

* `path` string
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/api/electron_api_app.cc
Expand Up @@ -66,6 +66,7 @@
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/language_util.h"
#include "shell/common/node_includes.h"
#include "shell/common/options_switches.h"
#include "shell/common/platform_util.h"
Expand Down Expand Up @@ -1790,6 +1791,7 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod("setAppLogsPath", &App::SetAppLogsPath)
.SetMethod("setDesktopName", &App::SetDesktopName)
.SetMethod("getLocale", &App::GetLocale)
.SetMethod("getPreferredSystemLanguages", &GetPreferredLanguages)
.SetMethod("getSystemLocale", &App::GetSystemLocale)
.SetMethod("getLocaleCountryCode", &App::GetLocaleCountryCode)
#if BUILDFLAG(USE_NSS_CERTS)
Expand Down
25 changes: 21 additions & 4 deletions shell/common/language_util_linux.cc
Expand Up @@ -4,14 +4,31 @@

#include "shell/common/language_util.h"

#include "ui/base/l10n/l10n_util.h"
#include <glib.h>

#include "base/check.h"
#include "base/i18n/rtl.h"

namespace electron {

std::vector<std::string> GetPreferredLanguages() {
// Return empty as there's no API to use. You may be able to use
// GetApplicationLocale() of a browser process.
return std::vector<std::string>{};
std::vector<std::string> preferredLanguages;

// Based on
// https://source.chromium.org/chromium/chromium/src/+/refs/tags/108.0.5329.0:ui/base/l10n/l10n_util.cc;l=543-554
// GLib implements correct environment variable parsing with
// the precedence order: LANGUAGE, LC_ALL, LC_MESSAGES and LANG.
const char* const* languages = g_get_language_names();
DCHECK(languages); // A valid pointer is guaranteed.
DCHECK(*languages); // At least one entry, "C", is guaranteed.

for (; *languages; ++languages) {
if (strcmp(*languages, "C") != 0) {
preferredLanguages.push_back(base::i18n::GetCanonicalLocale(*languages));
}
}

return preferredLanguages;
}

} // namespace electron
13 changes: 13 additions & 0 deletions spec-main/api-app-spec.ts
Expand Up @@ -124,6 +124,19 @@ describe('app module', () => {
});
});

describe('app.getPreferredSystemLanguages()', () => {
ifit(process.platform !== 'linux')('should not be empty', () => {
expect(app.getPreferredSystemLanguages().length).to.not.equal(0);
});

ifit(process.platform === 'linux')('should be empty or contain C entry', () => {
const languages = app.getPreferredSystemLanguages();
if (languages.length) {
expect(languages).to.not.include('C');
}
});
});

describe('app.getLocaleCountryCode()', () => {
it('should be empty or have length of two', () => {
const localeCountryCode = app.getLocaleCountryCode();
Expand Down
7 changes: 4 additions & 3 deletions spec-main/chromium-spec.ts
Expand Up @@ -375,6 +375,7 @@ describe('command line switches', () => {
describe('--lang switch', () => {
const currentLocale = app.getLocale();
const currentSystemLocale = app.getSystemLocale();
const currentPreferredLanguages = JSON.stringify(app.getPreferredSystemLanguages());
const testLocale = async (locale: string, result: string, printEnv: boolean = false) => {
const appPath = path.join(fixturesPath, 'api', 'locale-check');
const args = [appPath, `--set-lang=${locale}`];
Expand All @@ -397,9 +398,9 @@ describe('command line switches', () => {
expect(output).to.equal(result);
};

it('should set the locale', async () => testLocale('fr', `fr|${currentSystemLocale}`));
it('should set the locale with country code', async () => testLocale('zh-CN', `zh-CN|${currentSystemLocale}`));
it('should not set an invalid locale', async () => testLocale('asdfkl', `${currentLocale}|${currentSystemLocale}`));
it('should set the locale', async () => testLocale('fr', `fr|${currentSystemLocale}|${currentPreferredLanguages}`));
it('should set the locale with country code', async () => testLocale('zh-CN', `zh-CN|${currentSystemLocale}|${currentPreferredLanguages}`));
it('should not set an invalid locale', async () => testLocale('asdfkl', `${currentLocale}|${currentSystemLocale}|${currentPreferredLanguages}`));

const lcAll = String(process.env.LC_ALL);
ifit(process.platform === 'linux')('current process has a valid LC_ALL env', async () => {
Expand Down
2 changes: 1 addition & 1 deletion spec/fixtures/api/locale-check/main.js
Expand Up @@ -9,7 +9,7 @@ app.whenReady().then(() => {
if (process.argv[3] === '--print-env') {
process.stdout.write(String(process.env.LC_ALL));
} else {
process.stdout.write(`${app.getLocale()}|${app.getSystemLocale()}`);
process.stdout.write(`${app.getLocale()}|${app.getSystemLocale()}|${JSON.stringify(app.getPreferredSystemLanguages())}`);
}
process.stdout.end();

Expand Down

0 comments on commit d8a1298

Please sign in to comment.