@digestseo/mcp-gsc 0.4.29 → 0.4.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/google.ts +22 -0
- package/src/index.ts +86 -67
- package/src/url-inspection-language-schema.ts +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.31] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Gemini CLI can now install this repository as a native extension, bundling the verified local Streamable HTTP MCP connection with the existing `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities` Agent Skills.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Exact Search Analytics country and device filters now reject malformed provider values before reserving local Search Analytics budget, resolving Google credentials, or calling Search Console; substring and RE2 regex operators remain unrestricted.
|
|
17
|
+
- `sites.list` and `sites.get` now expose explicit `verification_state` and `owner` fields alongside Google's raw `permissionLevel`, so agents can distinguish unverified properties and owner-only Indexing API prerequisites before choosing a property workflow.
|
|
18
|
+
|
|
19
|
+
## [0.4.30] - 2026-09-23
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Search Analytics filters now default an omitted operator to Google's documented `equals` behavior in both `analytics.query` and `analytics.compare`, while both tools enforce the provider's 4096-character filter-expression ceiling through one shared schema.
|
|
23
|
+
- URL Inspection language codes are now validated as BCP-47 language tags before `urls.inspect` or `urls.inspect_many` can reserve local inspection budget, resolve Google credentials, or call the provider.
|
|
24
|
+
|
|
10
25
|
## [0.4.29] - 2026-09-23
|
|
11
26
|
|
|
12
27
|
### Fixed
|
|
@@ -192,6 +207,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
192
207
|
## [0.4.2] - 2026-09-21
|
|
193
208
|
|
|
194
209
|
### Fixed
|
|
210
|
+
- Search Analytics now rejects malformed exact-match country/device filters during MCP input validation, before local rate-limit, credential, or Google API work, while preserving contains and regex filter semantics.
|
|
195
211
|
- Cannibalization totals and impression shares now expose their query/page-row aggregation scope explicitly so clients do not mistake summed page impressions for a true query-level Search Console total.
|
|
196
212
|
- Discover Search Analytics calls now reject unsupported query grouping/filtering before reaching Google, and query-centric/comparison tools no longer advertise Discover where their contracts depend on query or average-position data that Discover does not provide.
|
|
197
213
|
- Quick-win guidance now treats Search Console position as an aggregate average-position metric rather than a literal current rank, and metadata no longer implies CTR is an eligibility filter when the implementation does not filter on CTR.
|
package/README.md
CHANGED
|
@@ -75,6 +75,14 @@ copilot plugin install mcp-gsc@digestseo
|
|
|
75
75
|
|
|
76
76
|
The marketplace path makes the plugin discoverable and updateable through Copilot's marketplace workflow. Direct repository install also remains supported with `copilot plugin install AKzar1el/mcp-gsc`. The Copilot plugin bundles the same three Agent Skills plus the local loopback MCP configuration. Start the npm launcher before using the plugin's MCP server; the plugin does not start or deploy mcp-gsc for you.
|
|
77
77
|
|
|
78
|
+
**Gemini CLI**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gemini extensions install https://github.com/AKzar1el/mcp-gsc --ref main
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The Gemini CLI extension bundles the verified local loopback MCP configuration plus the same three Agent Skills: `gsc-weekly-review`, `gsc-indexing-triage`, and `gsc-search-opportunities`. Start the npm launcher before using the extension; the extension does not start or deploy mcp-gsc for you.
|
|
85
|
+
|
|
78
86
|
**Kiro**
|
|
79
87
|
|
|
80
88
|
[](https://kiro.dev/launch/mcp/add?name=mcp-gsc&config=%7B%22url%22%3A%22http%3A%2F%2F127.0.0.1%3A8080%2Fmcp%22%2C%22disabled%22%3Afalse%2C%22autoApprove%22%3A%5B%5D%7D)
|
package/package.json
CHANGED
package/src/google.ts
CHANGED
|
@@ -314,6 +314,27 @@ export interface DimensionFilter {
|
|
|
314
314
|
expression: string;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
export function assertSearchAnalyticsFilterCompatible(
|
|
318
|
+
filter: DimensionFilter,
|
|
319
|
+
): void {
|
|
320
|
+
if (filter.operator !== 'equals' && filter.operator !== 'notEquals') return;
|
|
321
|
+
|
|
322
|
+
if (filter.dimension === 'country' && !/^[A-Za-z]{3}$/.test(filter.expression)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'Search Analytics exact country filters must use a three-letter ISO 3166-1 alpha-3 code.',
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
filter.dimension === 'device' &&
|
|
330
|
+
!['DESKTOP', 'MOBILE', 'TABLET'].includes(filter.expression.toUpperCase())
|
|
331
|
+
) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
'Search Analytics exact device filters must use DESKTOP, MOBILE, or TABLET.',
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
317
338
|
export interface DimensionFilterGroup {
|
|
318
339
|
groupType: 'and';
|
|
319
340
|
filters: DimensionFilter[];
|
|
@@ -369,6 +390,7 @@ export function assertSearchAnalyticsQueryCompatible(body: SearchAnalyticsQuery)
|
|
|
369
390
|
if (filter.expression.length > 4096) {
|
|
370
391
|
throw new Error('Search Analytics filter expressions must be at most 4096 characters.');
|
|
371
392
|
}
|
|
393
|
+
assertSearchAnalyticsFilterCompatible(filter);
|
|
372
394
|
}
|
|
373
395
|
|
|
374
396
|
if (body.aggregationType === 'byPage' && hasPageGroupingOrFilter) {
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
requestIndexingRemoval,
|
|
33
33
|
getIndexingNotificationMetadata,
|
|
34
34
|
processPerformanceComparison,
|
|
35
|
+
assertSearchAnalyticsFilterCompatible,
|
|
35
36
|
type PaginatedSearchAnalyticsResult,
|
|
36
37
|
} from './google';
|
|
37
38
|
import {
|
|
@@ -66,6 +67,7 @@ import { CONTENT_DECAY_COMPARE_DAYS_SCHEMA } from './content-decay-schema';
|
|
|
66
67
|
import { resolveIndexedPagesDateRange } from './indexed-pages-range';
|
|
67
68
|
import { createQuickWinsInputSchema } from './quick-wins-schema';
|
|
68
69
|
import { SITEMAP_URL_SCHEMA } from './sitemap-url-schema';
|
|
70
|
+
import { URL_INSPECTION_LANGUAGE_CODE_SCHEMA } from './url-inspection-language-schema';
|
|
69
71
|
import {
|
|
70
72
|
classifySearchConsolePropertyIdentifier,
|
|
71
73
|
SEARCH_CONSOLE_PROPERTY_DESCRIPTION,
|
|
@@ -148,9 +150,57 @@ const SEARCH_ROW_OUTPUT_SCHEMA = {
|
|
|
148
150
|
),
|
|
149
151
|
};
|
|
150
152
|
|
|
153
|
+
const SEARCH_ANALYTICS_FILTER_SCHEMA = z
|
|
154
|
+
.object({
|
|
155
|
+
dimension: z.enum([
|
|
156
|
+
'query',
|
|
157
|
+
'page',
|
|
158
|
+
'country',
|
|
159
|
+
'device',
|
|
160
|
+
'searchAppearance',
|
|
161
|
+
]),
|
|
162
|
+
operator: z
|
|
163
|
+
.enum([
|
|
164
|
+
'equals',
|
|
165
|
+
'notEquals',
|
|
166
|
+
'contains',
|
|
167
|
+
'notContains',
|
|
168
|
+
'includingRegex',
|
|
169
|
+
'excludingRegex',
|
|
170
|
+
])
|
|
171
|
+
.default('equals'),
|
|
172
|
+
expression: z.string().max(4096),
|
|
173
|
+
})
|
|
174
|
+
.superRefine((filter, ctx) => {
|
|
175
|
+
try {
|
|
176
|
+
assertSearchAnalyticsFilterCompatible(filter);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
ctx.addIssue({
|
|
179
|
+
code: 'custom',
|
|
180
|
+
path: ['expression'],
|
|
181
|
+
message: (error as Error).message,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA = z.object({
|
|
187
|
+
groupType: z.literal('and').default('and'),
|
|
188
|
+
filters: z.array(SEARCH_ANALYTICS_FILTER_SCHEMA),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const SITE_VERIFICATION_STATE_SCHEMA = z
|
|
192
|
+
.enum(['verified', 'unverified', 'unknown'])
|
|
193
|
+
.describe(
|
|
194
|
+
"Derived from Google's permissionLevel. siteOwner/siteFullUser/siteRestrictedUser are verified, siteUnverifiedUser is unverified, and any future undocumented permission level is reported as unknown.",
|
|
195
|
+
);
|
|
196
|
+
|
|
151
197
|
const SITE_OUTPUT_SCHEMA = {
|
|
152
198
|
siteUrl: z.string(),
|
|
153
199
|
permissionLevel: z.string(),
|
|
200
|
+
verification_state: SITE_VERIFICATION_STATE_SCHEMA,
|
|
201
|
+
owner: z.boolean().describe(
|
|
202
|
+
"True only when Google returns permissionLevel 'siteOwner'. Restricted Indexing API workflows require owner-level Search Console access.",
|
|
203
|
+
),
|
|
154
204
|
};
|
|
155
205
|
|
|
156
206
|
const LISTED_SITE_OUTPUT_SCHEMA = {
|
|
@@ -163,6 +213,26 @@ const LISTED_SITE_OUTPUT_SCHEMA = {
|
|
|
163
213
|
const PLATFORM_PROPERTY_API_NOTE =
|
|
164
214
|
"Google Search Console now has platform properties for social/video accounts in its UI, but the current Search Console API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms. mcp-gsc preserves any other identifier returned by sites.list but will not pass an undocumented property identifier to API tools until Google publishes that contract.";
|
|
165
215
|
|
|
216
|
+
function sitePermissionState(permissionLevel: string): {
|
|
217
|
+
verification_state: 'verified' | 'unverified' | 'unknown';
|
|
218
|
+
owner: boolean;
|
|
219
|
+
} {
|
|
220
|
+
if (permissionLevel === 'siteUnverifiedUser') {
|
|
221
|
+
return { verification_state: 'unverified', owner: false };
|
|
222
|
+
}
|
|
223
|
+
if (
|
|
224
|
+
permissionLevel === 'siteOwner' ||
|
|
225
|
+
permissionLevel === 'siteFullUser' ||
|
|
226
|
+
permissionLevel === 'siteRestrictedUser'
|
|
227
|
+
) {
|
|
228
|
+
return {
|
|
229
|
+
verification_state: 'verified',
|
|
230
|
+
owner: permissionLevel === 'siteOwner',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { verification_state: 'unknown', owner: false };
|
|
234
|
+
}
|
|
235
|
+
|
|
166
236
|
const SITEMAP_PROVIDER_NOTE =
|
|
167
237
|
"Google's lastSubmitted is the time the sitemap was submitted to Search Console; it is not the sitemap file's generation, deployment, or last-modified time. lastDownloaded is the time Google last downloaded the sitemap; it is not a page crawl or indexing timestamp.";
|
|
168
238
|
|
|
@@ -887,7 +957,7 @@ class GscMcpRuntime {
|
|
|
887
957
|
{
|
|
888
958
|
title: 'List Search Console properties',
|
|
889
959
|
description:
|
|
890
|
-
"List the Google Search Console properties (sites) the connected Google account has access to. Each entry preserves Google's siteUrl and permissionLevel and adds api_identifier_kind plus mcp_site_url_accepted. Google now exposes platform properties for social/video accounts in the Search Console UI, but its current API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms; identifiers outside those documented forms are preserved here but marked unsupported rather than guessed. Call this when the user asks 'what sites do I have?' or 'what properties are connected?', or before another tool that requires site_url.",
|
|
960
|
+
"List the Google Search Console properties (sites) the connected Google account has access to. Each entry preserves Google's siteUrl and permissionLevel, derives verification_state plus owner from Google's documented permission levels, and adds api_identifier_kind plus mcp_site_url_accepted. Treat verification_state='unverified' as not ready for normal property-scoped Search Console workflows; owner=true is required by this server's restricted Indexing API workflows. Google now exposes platform properties for social/video accounts in the Search Console UI, but its current API documentation still defines siteUrl using URL-prefix and sc-domain website-property forms; identifiers outside those documented forms are preserved here but marked unsupported rather than guessed. Call this when the user asks 'what sites do I have?' or 'what properties are connected?', or before another tool that requires site_url.",
|
|
891
961
|
inputSchema: {},
|
|
892
962
|
outputSchema: SITES_OUTPUT_SCHEMA,
|
|
893
963
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -900,6 +970,7 @@ class GscMcpRuntime {
|
|
|
900
970
|
const mcpSiteUrlAccepted = apiIdentifierKind !== 'undocumented';
|
|
901
971
|
return {
|
|
902
972
|
...site,
|
|
973
|
+
...sitePermissionState(site.permissionLevel),
|
|
903
974
|
api_identifier_kind: apiIdentifierKind,
|
|
904
975
|
mcp_site_url_accepted: mcpSiteUrlAccepted,
|
|
905
976
|
...(!mcpSiteUrlAccepted
|
|
@@ -916,7 +987,7 @@ class GscMcpRuntime {
|
|
|
916
987
|
{
|
|
917
988
|
title: 'Get Search Console property',
|
|
918
989
|
description:
|
|
919
|
-
"Retrieve one exact Google Search Console property and
|
|
990
|
+
"Retrieve one exact Google Search Console property and its connected-account access state. The response preserves Google's permissionLevel and derives verification_state plus owner so callers can distinguish an unverified property and recognize owner-only Indexing API prerequisites without listing every property first.",
|
|
920
991
|
inputSchema: {
|
|
921
992
|
site_url: SEARCH_CONSOLE_PROPERTY_SCHEMA,
|
|
922
993
|
},
|
|
@@ -927,7 +998,13 @@ class GscMcpRuntime {
|
|
|
927
998
|
const googleId = this.requireGoogleId();
|
|
928
999
|
const accessToken = await this.getAccessToken(googleId);
|
|
929
1000
|
const site = await getSite(accessToken, site_url);
|
|
930
|
-
|
|
1001
|
+
const siteWithPermissionState = {
|
|
1002
|
+
...site,
|
|
1003
|
+
...sitePermissionState(site.permissionLevel),
|
|
1004
|
+
};
|
|
1005
|
+
return toolResponse(JSON.stringify(siteWithPermissionState, null, 2), {
|
|
1006
|
+
site: siteWithPermissionState,
|
|
1007
|
+
});
|
|
931
1008
|
},
|
|
932
1009
|
);
|
|
933
1010
|
|
|
@@ -943,12 +1020,7 @@ class GscMcpRuntime {
|
|
|
943
1020
|
.describe(
|
|
944
1021
|
'The fully-qualified URL to inspect. Must belong to the site_url property: same domain for sc-domain properties, same URL prefix for URL-prefix properties.',
|
|
945
1022
|
),
|
|
946
|
-
language_code:
|
|
947
|
-
.string()
|
|
948
|
-
.default('en-US')
|
|
949
|
-
.describe(
|
|
950
|
-
"BCP-47 language code for translatable strings in the result, e.g. 'en-US' or 'de-DE'.",
|
|
951
|
-
),
|
|
1023
|
+
language_code: URL_INSPECTION_LANGUAGE_CODE_SCHEMA,
|
|
952
1024
|
},
|
|
953
1025
|
outputSchema: INSPECTION_OUTPUT_SCHEMA,
|
|
954
1026
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -993,12 +1065,7 @@ class GscMcpRuntime {
|
|
|
993
1065
|
.describe(
|
|
994
1066
|
'Between 1 and 10 unique fully-qualified URLs to inspect. Each URL must belong to the site_url property.',
|
|
995
1067
|
),
|
|
996
|
-
language_code:
|
|
997
|
-
.string()
|
|
998
|
-
.default('en-US')
|
|
999
|
-
.describe(
|
|
1000
|
-
"BCP-47 language code for translatable strings in the results, e.g. 'en-US' or 'de-DE'.",
|
|
1001
|
-
),
|
|
1068
|
+
language_code: URL_INSPECTION_LANGUAGE_CODE_SCHEMA,
|
|
1002
1069
|
},
|
|
1003
1070
|
outputSchema: INSPECTION_BATCH_OUTPUT_SCHEMA,
|
|
1004
1071
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
@@ -1222,34 +1289,10 @@ class GscMcpRuntime {
|
|
|
1222
1289
|
"How Google aggregates metrics. Leave as 'auto' unless specific semantics are needed. When grouping/filtering by page, use 'auto'; explicit 'byPage' and 'byProperty' are rejected for that request shape. 'byProperty' is also unavailable for search_type discover/googleNews. 'byNewsShowcasePanel' requires search_type discover/googleNews plus a searchAppearance equals NEWS_SHOWCASE filter, and cannot be combined with page grouping/filtering or another searchAppearance filter.",
|
|
1223
1290
|
),
|
|
1224
1291
|
dimension_filter_groups: z
|
|
1225
|
-
.array(
|
|
1226
|
-
z.object({
|
|
1227
|
-
groupType: z.literal('and').default('and'),
|
|
1228
|
-
filters: z.array(
|
|
1229
|
-
z.object({
|
|
1230
|
-
dimension: z.enum([
|
|
1231
|
-
'query',
|
|
1232
|
-
'page',
|
|
1233
|
-
'country',
|
|
1234
|
-
'device',
|
|
1235
|
-
'searchAppearance',
|
|
1236
|
-
]),
|
|
1237
|
-
operator: z.enum([
|
|
1238
|
-
'equals',
|
|
1239
|
-
'notEquals',
|
|
1240
|
-
'contains',
|
|
1241
|
-
'notContains',
|
|
1242
|
-
'includingRegex',
|
|
1243
|
-
'excludingRegex',
|
|
1244
|
-
]),
|
|
1245
|
-
expression: z.string().max(4096),
|
|
1246
|
-
}),
|
|
1247
|
-
),
|
|
1248
|
-
}),
|
|
1249
|
-
)
|
|
1292
|
+
.array(SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA)
|
|
1250
1293
|
.optional()
|
|
1251
1294
|
.describe(
|
|
1252
|
-
"Optional filters ANDed together, e.g. [{ groupType: 'and', filters: [{ dimension: 'country',
|
|
1295
|
+
"Optional filters ANDed together, e.g. [{ groupType: 'and', filters: [{ dimension: 'country', expression: 'usa' }] }]. Filter operator defaults to 'equals' when omitted, matching Search Console. Expressions are limited to 4096 characters. Countries use ISO 3166-1 alpha-3 codes. Query filters are not supported when search_type is discover or googleNews.",
|
|
1253
1296
|
),
|
|
1254
1297
|
},
|
|
1255
1298
|
outputSchema: SEARCH_ANALYTICS_OUTPUT_SCHEMA,
|
|
@@ -2021,34 +2064,10 @@ class GscMcpRuntime {
|
|
|
2021
2064
|
.default('web')
|
|
2022
2065
|
.describe('Which Search Console search index to compare. The same search type is used for both periods. Discover and Google News are excluded because this tool returns average-position comparisons.'),
|
|
2023
2066
|
dimension_filter_groups: z
|
|
2024
|
-
.array(
|
|
2025
|
-
z.object({
|
|
2026
|
-
groupType: z.literal('and').default('and'),
|
|
2027
|
-
filters: z.array(
|
|
2028
|
-
z.object({
|
|
2029
|
-
dimension: z.enum([
|
|
2030
|
-
'query',
|
|
2031
|
-
'page',
|
|
2032
|
-
'country',
|
|
2033
|
-
'device',
|
|
2034
|
-
'searchAppearance',
|
|
2035
|
-
]),
|
|
2036
|
-
operator: z.enum([
|
|
2037
|
-
'equals',
|
|
2038
|
-
'notEquals',
|
|
2039
|
-
'contains',
|
|
2040
|
-
'notContains',
|
|
2041
|
-
'includingRegex',
|
|
2042
|
-
'excludingRegex',
|
|
2043
|
-
]),
|
|
2044
|
-
expression: z.string(),
|
|
2045
|
-
}),
|
|
2046
|
-
),
|
|
2047
|
-
}),
|
|
2048
|
-
)
|
|
2067
|
+
.array(SEARCH_ANALYTICS_FILTER_GROUP_SCHEMA)
|
|
2049
2068
|
.optional()
|
|
2050
2069
|
.describe(
|
|
2051
|
-
"Optional Search Console filters applied identically to both periods. A caller-supplied query regex can approximate a manual brand/non-brand split, but it is not equivalent to Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose. Countries use ISO 3166-1 alpha-3 codes.",
|
|
2070
|
+
"Optional Search Console filters applied identically to both periods. Filter operator defaults to 'equals' when omitted, and expressions are limited to 4096 characters. A caller-supplied query regex can approximate a manual brand/non-brand split, but it is not equivalent to Search Console's AI-assisted native Branded/Non-branded filter, which the Search Analytics API does not expose. Countries use ISO 3166-1 alpha-3 codes.",
|
|
2052
2071
|
),
|
|
2053
2072
|
limit: ANALYSIS_RESULT_LIMIT_SCHEMA,
|
|
2054
2073
|
start_row: RESULT_START_ROW_SCHEMA,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const URL_INSPECTION_LANGUAGE_CODE_SCHEMA = z
|
|
4
|
+
.string()
|
|
5
|
+
.superRefine((value, ctx) => {
|
|
6
|
+
try {
|
|
7
|
+
if (Intl.getCanonicalLocales(value).length !== 1) {
|
|
8
|
+
throw new RangeError('Invalid language tag');
|
|
9
|
+
}
|
|
10
|
+
} catch {
|
|
11
|
+
ctx.addIssue({
|
|
12
|
+
code: 'custom',
|
|
13
|
+
message:
|
|
14
|
+
"language_code must be a valid BCP-47 language tag, e.g. 'en-US' or 'de-CH'.",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.default('en-US')
|
|
19
|
+
.describe(
|
|
20
|
+
"BCP-47 language code for translatable strings in the result, e.g. 'en-US' or 'de-CH'.",
|
|
21
|
+
);
|