Skip to content

Commit 8cefe61

Browse files
feat: implement emulation.setLocaleOverride (#3425)
Spec is landed: w3c/webdriver-bidi#924
1 parent e523161 commit 8cefe61

File tree

10 files changed

+446
-2
lines changed

10 files changed

+446
-2
lines changed

src/bidiMapper/BidiNoOpParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ export class BidiNoOpParser implements BidiCommandParameterParser {
181181
): Emulation.SetGeolocationOverrideParameters {
182182
return params as Emulation.SetGeolocationOverrideParameters;
183183
}
184+
parseSetLocaleOverrideParams(
185+
params: unknown,
186+
): Emulation.SetLocaleOverrideParameters {
187+
return params as Emulation.SetLocaleOverrideParameters;
188+
}
184189
parseSetScreenOrientationOverrideParams(
185190
params: unknown,
186191
): Emulation.SetScreenOrientationOverrideParameters {

src/bidiMapper/BidiParser.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export interface BidiCommandParameterParser {
119119
parseSetGeolocationOverrideParams(
120120
params: unknown,
121121
): Emulation.SetGeolocationOverrideParameters;
122+
parseSetLocaleOverrideParams(
123+
params: unknown,
124+
): Emulation.SetLocaleOverrideParameters;
122125
parseSetScreenOrientationOverrideParams(
123126
params: unknown,
124127
): Emulation.SetScreenOrientationOverrideParameters;

src/bidiMapper/CommandProcessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,8 @@ export class CommandProcessor extends EventEmitter<CommandProcessorEventsMap> {
317317
this.#parser.parseSetGeolocationOverrideParams(command.params),
318318
);
319319
case 'emulation.setLocaleOverride':
320-
throw new UnknownErrorException(
321-
`Method ${command.method} is not implemented.`,
320+
return await this.#emulationProcessor.setLocaleOverride(
321+
this.#parser.parseSetLocaleOverrideParams(command.params),
322322
);
323323
case 'emulation.setScreenOrientationOverride':
324324
return await this.#emulationProcessor.setScreenOrientationOverride(

src/bidiMapper/modules/browser/UserContextConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class UserContextConfig {
3333
| Emulation.GeolocationCoordinates
3434
| Emulation.GeolocationPositionError
3535
| null;
36+
locale?: string | null;
3637
screenOrientation?: Emulation.ScreenOrientation | null;
3738

3839
constructor(userContextId: string) {

src/bidiMapper/modules/cdp/CdpTarget.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,10 @@ export class CdpTarget {
661661
);
662662
}
663663

664+
if (this.#userContextConfig.locale !== undefined) {
665+
promises.push(this.setLocaleOverride(this.#userContextConfig.locale));
666+
}
667+
664668
if (this.#userContextConfig.acceptInsecureCerts !== undefined) {
665669
promises.push(
666670
this.cdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
@@ -815,4 +819,14 @@ export class CdpTarget {
815819
`Unexpected orientation natural ${orientation.natural}`,
816820
);
817821
}
822+
823+
async setLocaleOverride(locale: string | null): Promise<void> {
824+
if (locale === null) {
825+
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {});
826+
} else {
827+
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {
828+
locale,
829+
});
830+
}
831+
}
818832
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2025 Google LLC.
3+
* Copyright (c) Microsoft Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
import {expect} from 'chai';
20+
21+
import {isValidLocale} from './EmulationProcessor.js';
22+
23+
describe('EmulationProcessor.isValidLocale', () => {
24+
const invalidLocales = [
25+
// Is an empty string.
26+
'',
27+
// Language subtag is too short.
28+
'a',
29+
// Language subtag is too long.
30+
'abcd',
31+
// Language subtag contains invalid characters (numbers).
32+
'12',
33+
// Language subtag contains invalid characters (symbols).
34+
'en$',
35+
// Uses underscore instead of hyphen as a separator.
36+
'en_US',
37+
// Region subtag is too short.
38+
'en-U',
39+
// Region subtag is too long.
40+
'en-USAXASDASD',
41+
// Region subtag contains invalid characters (numbers not part of a valid UN M49 code).
42+
'en-U1',
43+
// Region subtag contains invalid characters (symbols).
44+
'en-US$',
45+
// Script subtag is too short.
46+
'en-Lat',
47+
// Script subtag is too long.
48+
'en-Somelongsubtag',
49+
// Script subtag contains invalid characters (numbers).
50+
'en-La1n',
51+
// Script subtag contains invalid characters (symbols).
52+
'en-Lat$',
53+
// Variant subtag is too short (must be 5-8 alphanumeric chars, or 4 if starting with a digit).
54+
'en-US-var',
55+
// Variant subtag contains invalid characters (symbols).
56+
'en-US-variant$',
57+
// Extension subtag is malformed (singleton 'u' not followed by anything).
58+
'en-u-',
59+
// Extension subtag is malformed (singleton 't' not followed by anything).
60+
'de-t-',
61+
// Private use subtag 'x-' is not followed by anything.
62+
'x-',
63+
// Locale consisting only of a private use subtag.
64+
'x-another-private-tag',
65+
// Private use subtag contains invalid characters (underscore).
66+
'en-x-private_use',
67+
// Contains an empty subtag (double hyphen).
68+
'en--US',
69+
// Starts with a hyphen.
70+
'-en-US',
71+
// Ends with a hyphen.
72+
'en-US-',
73+
// Contains only a hyphen.
74+
'-',
75+
// Contains non-ASCII characters.
76+
'en-US-ñ',
77+
// Grandfathered tag with invalid structure.
78+
'i-notarealtag',
79+
// Invalid UN M49 region code (not 3 digits).
80+
'en-01',
81+
// Invalid UN M49 region code (contains letters).
82+
'en-0A1',
83+
// Malformed language tag with numbers.
84+
'123',
85+
// Locale with only script.
86+
'Latn',
87+
// Locale with script before language.
88+
'Latn-en',
89+
// Repeated separator.
90+
'en--US',
91+
// Invalid character in an otherwise valid structure.
92+
'en-US-!',
93+
// Too many subtags of a specific type (e.g., multiple script tags).
94+
'en-Latn-Cyrl-US',
95+
];
96+
97+
invalidLocales.forEach((locale) => {
98+
it(`should return false for invalid locale: "${locale}"`, () => {
99+
expect(isValidLocale(locale), `"${locale}" should be invalid`).to.be
100+
.false;
101+
});
102+
});
103+
104+
const validLocales = [
105+
// Simple language code (2-letter).
106+
'en',
107+
// Simple language code (3-letter ISO 639-2/3).
108+
'ast',
109+
// Language and region (both 2-letter).
110+
'en-US',
111+
// Language and script (4-letter).
112+
'sr-Latn',
113+
// Language, script, and region.
114+
'zh-Hans-CN',
115+
// Language and variant (longer variant).
116+
'de-DE-1996',
117+
// Language and multiple variants.
118+
'sl-Roza-biske',
119+
// Language, region, and variant.
120+
'ca-ES-valencia',
121+
// Language and variant (4-char variant starting with digit).
122+
'sl-1994',
123+
// Locale with Unicode extension keyword for numbering system.
124+
'th-TH-u-nu-thai',
125+
// Locale with Unicode extension for calendar.
126+
'en-US-u-ca-gregory',
127+
// Canonicalized extended language subtag (Yue Chinese).
128+
'yue',
129+
// Canonicalized extended language subtag (North Levantine Arabic).
130+
'apc',
131+
// Language with a less common but valid 3-letter code.
132+
'gsw',
133+
// A complex but valid tag with multiple subtags including extension and private use.
134+
'zh-Latn-CN-variant1-a-extend1-u-co-pinyin-x-private',
135+
// Locale with Unicode extension keyword for collation.
136+
'de-DE-u-co-phonebk',
137+
// Lowercase language and region.
138+
'fr-ca',
139+
// Uppercase language and region (should be normalized by Intl.Locale).
140+
'FR-CA',
141+
// Mixed case language and region (should be normalized by Intl.Locale).
142+
'fR-cA',
143+
// Locale with transform extension (simple case).
144+
'en-t-zh',
145+
// Language (2-letter) and region (3-digit UN M49).
146+
'es-419',
147+
];
148+
149+
validLocales.forEach((locale) => {
150+
it(`should return true for valid locale: "${locale}"`, () => {
151+
expect(isValidLocale(locale), `"${locale}" should be valid`).to.be.true;
152+
});
153+
});
154+
});

src/bidiMapper/modules/emulation/EmulationProcessor.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,34 @@ export class EmulationProcessor {
9595
return {};
9696
}
9797

98+
async setLocaleOverride(
99+
params: Emulation.SetLocaleOverrideParameters,
100+
): Promise<EmptyResult> {
101+
const locale = params.locale ?? null;
102+
103+
if (locale !== null && !isValidLocale(locale)) {
104+
throw new InvalidArgumentException(`Invalid locale "${locale}"`);
105+
}
106+
107+
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(
108+
params.contexts,
109+
params.userContexts,
110+
);
111+
112+
for (const userContextId of params.userContexts ?? []) {
113+
const userContextConfig =
114+
this.#userContextStorage.getConfig(userContextId);
115+
userContextConfig.locale = locale;
116+
}
117+
118+
await Promise.all(
119+
browsingContexts.map(
120+
async (context) => await context.cdpTarget.setLocaleOverride(locale),
121+
),
122+
);
123+
return {};
124+
}
125+
98126
async setScreenOrientationOverride(
99127
params: Emulation.SetScreenOrientationOverrideParameters,
100128
): Promise<EmptyResult> {
@@ -180,3 +208,17 @@ export class EmulationProcessor {
180208
return [...new Set(result).values()];
181209
}
182210
}
211+
212+
// Export for testing.
213+
export function isValidLocale(locale: string): boolean {
214+
try {
215+
new Intl.Locale(locale);
216+
return true;
217+
} catch (e) {
218+
if (e instanceof RangeError) {
219+
return false;
220+
}
221+
// Re-throw other errors
222+
throw e;
223+
}
224+
}

src/bidiTab/BidiParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@ export class BidiParser implements BidiCommandParameterParser {
182182
): Emulation.SetGeolocationOverrideParameters {
183183
return Parser.Emulation.parseSetGeolocationOverrideParams(params);
184184
}
185+
parseSetLocaleOverrideParams(
186+
params: unknown,
187+
): Emulation.SetLocaleOverrideParameters {
188+
return Parser.Emulation.parseSetLocaleOverrideParams(params);
189+
}
185190
parseSetScreenOrientationOverrideParams(
186191
params: unknown,
187192
): Emulation.SetScreenOrientationOverrideParameters {

src/protocol-parser/protocol-parser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ export namespace Emulation {
318318
WebDriverBidi.Emulation.SetGeolocationOverrideParametersSchema,
319319
) as Protocol.Emulation.SetGeolocationOverrideParameters;
320320
}
321+
export function parseSetLocaleOverrideParams(params: unknown) {
322+
return parseObject(
323+
params,
324+
WebDriverBidi.Emulation.SetLocaleOverrideParametersSchema,
325+
) as Protocol.Emulation.SetLocaleOverrideParameters;
326+
}
321327
export function parseSetScreenOrientationOverrideParams(params: unknown) {
322328
return parseObject(
323329
params,

0 commit comments

Comments
 (0)