@prosopo/locale 3.4.3 → 3.6.0
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/.turbo/turbo-build$colon$cjs.log +15 -13
- package/.turbo/turbo-build$colon$tsc.log +8 -5
- package/.turbo/turbo-build.log +17 -15
- package/CHANGELOG.md +74 -0
- package/dist/cjs/i18SharedOptions.cjs +2 -2
- package/dist/cjs/i18nBackend.cjs +1 -1
- package/dist/cjs/i18nFrontend.cjs +167 -53
- package/dist/cjs/i18nMiddleware.cjs +5 -2
- package/dist/cjs/index.cjs +5 -4
- package/dist/cjs/locales/en/translation.cjs +1 -0
- package/dist/cjs/translationKey.cjs +2 -3
- package/dist/cjs/translations.cjs +4 -5
- package/dist/cjs/translator.cjs +29 -0
- package/dist/i18SharedOptions.d.ts +2 -2
- package/dist/i18SharedOptions.js +3 -3
- package/dist/i18SharedOptions.js.map +1 -1
- package/dist/i18nBackend.js +1 -1
- package/dist/i18nFrontend.d.ts +2 -2
- package/dist/i18nFrontend.d.ts.map +1 -1
- package/dist/i18nFrontend.js +167 -47
- package/dist/i18nFrontend.js.map +1 -1
- package/dist/i18nMiddleware.d.ts.map +1 -1
- package/dist/i18nMiddleware.js +3 -1
- package/dist/i18nMiddleware.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/loadI18next.d.ts +2 -3
- package/dist/loadI18next.d.ts.map +1 -1
- package/dist/loadI18next.js.map +1 -1
- package/dist/locales/ar/translation.json +1 -0
- package/dist/locales/az/translation.json +1 -0
- package/dist/locales/cs/translation.json +1 -0
- package/dist/locales/de/translation.json +1 -0
- package/dist/locales/el/translation.json +1 -0
- package/dist/locales/en/translation.js +1 -0
- package/dist/locales/en/translation.json +1 -0
- package/dist/locales/es/translation.json +1 -0
- package/dist/locales/fi/translation.json +1 -0
- package/dist/locales/fr/translation.json +1 -0
- package/dist/locales/hi/translation.json +1 -0
- package/dist/locales/hu/translation.json +1 -0
- package/dist/locales/id/translation.json +1 -0
- package/dist/locales/it/translation.json +1 -0
- package/dist/locales/ja/translation.json +1 -0
- package/dist/locales/jv/translation.json +1 -0
- package/dist/locales/ko/translation.json +1 -0
- package/dist/locales/ml/translation.json +1 -0
- package/dist/locales/ms/translation.json +1 -0
- package/dist/locales/nl/translation.json +1 -0
- package/dist/locales/no/translation.json +1 -0
- package/dist/locales/pl/translation.json +1 -0
- package/dist/locales/pt/translation.json +1 -0
- package/dist/locales/pt-BR/translation.json +1 -0
- package/dist/locales/ro/translation.json +1 -0
- package/dist/locales/ru/translation.json +1 -0
- package/dist/locales/sr/translation.json +1 -0
- package/dist/locales/sv/translation.json +1 -0
- package/dist/locales/th/translation.json +1 -0
- package/dist/locales/tr/translation.json +1 -0
- package/dist/locales/uk/translation.json +1 -0
- package/dist/locales/vi/translation.json +1 -0
- package/dist/locales/zh-CN/translation.json +1 -0
- package/dist/tests/i18nFrontend.unit.test.js +218 -173
- package/dist/tests/i18nFrontend.unit.test.js.map +1 -1
- package/dist/tests/i18nMiddleware.unit.test.js +11 -2
- package/dist/tests/i18nMiddleware.unit.test.js.map +1 -1
- package/dist/tests/loadI18next.unit.test.js.map +1 -1
- package/dist/tests/locale.test-d.js +27 -20
- package/dist/tests/locale.test-d.js.map +1 -1
- package/dist/tests/translationKey.unit.test.js +11 -33
- package/dist/tests/translationKey.unit.test.js.map +1 -1
- package/dist/tests/translations.unit.test.js +12 -22
- package/dist/tests/translations.unit.test.js.map +1 -1
- package/dist/tests/translator.unit.test.d.ts +2 -0
- package/dist/tests/translator.unit.test.d.ts.map +1 -0
- package/dist/tests/translator.unit.test.js +134 -0
- package/dist/tests/translator.unit.test.js.map +1 -0
- package/dist/translationKey.d.ts +2 -3
- package/dist/translationKey.d.ts.map +1 -1
- package/dist/translationKey.js +2 -3
- package/dist/translationKey.js.map +1 -1
- package/dist/translations.d.ts +3 -2
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +3 -3
- package/dist/translations.js.map +1 -1
- package/dist/translator.d.ts +9 -0
- package/dist/translator.d.ts.map +1 -0
- package/dist/translator.js +29 -0
- package/dist/translator.js.map +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +4 -9
- package/src/i18SharedOptions.ts +3 -3
- package/src/i18nFrontend.ts +240 -78
- package/src/i18nMiddleware.ts +6 -1
- package/src/index.ts +11 -4
- package/src/loadI18next.ts +8 -9
- package/src/locales/ar/translation.json +1 -0
- package/src/locales/az/translation.json +1 -0
- package/src/locales/cs/translation.json +1 -0
- package/src/locales/de/translation.json +1 -0
- package/src/locales/el/translation.json +1 -0
- package/src/locales/en/translation.json +1 -0
- package/src/locales/es/translation.json +1 -0
- package/src/locales/fi/translation.json +1 -0
- package/src/locales/fr/translation.json +1 -0
- package/src/locales/hi/translation.json +1 -0
- package/src/locales/hu/translation.json +1 -0
- package/src/locales/id/translation.json +1 -0
- package/src/locales/it/translation.json +1 -0
- package/src/locales/ja/translation.json +1 -0
- package/src/locales/jv/translation.json +1 -0
- package/src/locales/ko/translation.json +1 -0
- package/src/locales/ml/translation.json +1 -0
- package/src/locales/ms/translation.json +1 -0
- package/src/locales/nl/translation.json +1 -0
- package/src/locales/no/translation.json +1 -0
- package/src/locales/pl/translation.json +1 -0
- package/src/locales/pt/translation.json +1 -0
- package/src/locales/pt-BR/translation.json +1 -0
- package/src/locales/ro/translation.json +1 -0
- package/src/locales/ru/translation.json +1 -0
- package/src/locales/sr/translation.json +1 -0
- package/src/locales/sv/translation.json +1 -0
- package/src/locales/th/translation.json +1 -0
- package/src/locales/tr/translation.json +1 -0
- package/src/locales/uk/translation.json +1 -0
- package/src/locales/vi/translation.json +1 -0
- package/src/locales/zh-CN/translation.json +1 -0
- package/src/tests/i18nFrontend.unit.test.ts +310 -219
- package/src/tests/i18nMiddleware.unit.test.ts +21 -2
- package/src/tests/loadI18next.unit.test.ts +11 -11
- package/src/tests/locale.test-d.ts +36 -32
- package/src/tests/translationKey.unit.test.ts +12 -38
- package/src/tests/translations.unit.test.ts +18 -31
- package/src/tests/translator.unit.test.ts +213 -0
- package/src/translationKey.ts +10 -9
- package/src/translations.ts +6 -5
- package/src/translator.ts +68 -0
- package/src/types.ts +42 -0
- package/tsconfig.cjs.json +3 -0
- package/tsconfig.json +3 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/useTranslation.cjs +0 -12
- package/dist/tests/useTranslation.unit.test.d.ts +0 -2
- package/dist/tests/useTranslation.unit.test.d.ts.map +0 -1
- package/dist/tests/useTranslation.unit.test.js +0 -81
- package/dist/tests/useTranslation.unit.test.js.map +0 -1
- package/dist/useTranslation.d.ts +0 -7
- package/dist/useTranslation.d.ts.map +0 -1
- package/dist/useTranslation.js +0 -11
- package/dist/useTranslation.js.map +0 -1
- package/src/tests/useTranslation.unit.test.ts +0 -134
- package/src/useTranslation.ts +0 -31
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TranslateOptions {
|
|
2
|
+
defaultValue?: string;
|
|
3
|
+
[value: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export type TranslateFn = (key: string, options?: TranslateOptions) => string;
|
|
6
|
+
export type I18nEvent = "initialized" | "loaded" | "languageChanged";
|
|
7
|
+
export interface Ti18n {
|
|
8
|
+
readonly language: string;
|
|
9
|
+
readonly isInitialized: boolean;
|
|
10
|
+
t: TranslateFn;
|
|
11
|
+
changeLanguage(language: string): Promise<unknown>;
|
|
12
|
+
hasLoadedNamespace(namespace: string): boolean;
|
|
13
|
+
on(event: I18nEvent, listener: () => void): void;
|
|
14
|
+
off(event: I18nEvent, listener: () => void): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,gBAAgB;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,CAAC;AAE9E,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,QAAQ,GAAG,iBAAiB,CAAC;AAOrE,MAAM,WAAW,KAAK;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,CAAC,EAAE,WAAW,CAAC;IACf,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IACjD,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAClD"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/locale",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Prosopo locale library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -32,18 +32,13 @@
|
|
|
32
32
|
"author": "Prosopo Limited",
|
|
33
33
|
"license": "Apache-2.0",
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@prosopo/util": "3.3.11",
|
|
35
36
|
"i18next": "26.4.2",
|
|
36
|
-
"i18next-browser-languagedetector": "8.2.1",
|
|
37
|
-
"i18next-chained-backend": "4.6.2",
|
|
38
37
|
"i18next-fs-backend": "2.6.8",
|
|
39
|
-
"i18next-http-
|
|
40
|
-
"i18next-http-middleware": "3.9.9",
|
|
41
|
-
"i18next-resources-to-backend": "1.2.3",
|
|
42
|
-
"react-i18next": "17.0.13",
|
|
43
|
-
"zod": "3.23.8"
|
|
38
|
+
"i18next-http-middleware": "3.9.9"
|
|
44
39
|
},
|
|
45
40
|
"devDependencies": {
|
|
46
|
-
"@prosopo/config": "3.
|
|
41
|
+
"@prosopo/config": "3.4.0",
|
|
47
42
|
"@types/node": "22.10.2",
|
|
48
43
|
"@vitest/coverage-v8": "4.1.11",
|
|
49
44
|
"concurrently": "9.0.1",
|
package/src/i18SharedOptions.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
import { getProcess } from "./process.js";
|
|
15
|
-
import {
|
|
15
|
+
import { LanguageCodes, Languages } from "./translations.js";
|
|
16
16
|
|
|
17
17
|
// Read through getProcess() so the module stays loadable in a plain browser
|
|
18
18
|
// runtime where `process` is undefined. Without that guard, any side-effectful
|
|
@@ -22,8 +22,8 @@ const logLevel = getProcess()?.env?.PROSOPO_LOG_LEVEL;
|
|
|
22
22
|
|
|
23
23
|
export const i18nSharedOptions = {
|
|
24
24
|
debug: logLevel === "debug",
|
|
25
|
-
fallbackLng:
|
|
25
|
+
fallbackLng: Languages.english,
|
|
26
26
|
namespace: "translation",
|
|
27
|
-
supportedLngs:
|
|
27
|
+
supportedLngs: LanguageCodes,
|
|
28
28
|
nonExplicitSupportedLngs: false,
|
|
29
29
|
};
|
package/src/i18nFrontend.ts
CHANGED
|
@@ -12,94 +12,256 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import I18nextBrowserLanguageDetector from "i18next-browser-languagedetector";
|
|
17
|
-
import ChainedBackend from "i18next-chained-backend";
|
|
18
|
-
import HttpBackend from "i18next-http-backend";
|
|
19
|
-
import resourcesToBackend from "i18next-resources-to-backend";
|
|
20
|
-
import { initReactI18next } from "react-i18next";
|
|
15
|
+
import { flatten } from "@prosopo/util";
|
|
21
16
|
import { i18nSharedOptions } from "./i18SharedOptions.js";
|
|
17
|
+
import type { I18nEvent, Ti18n, TranslateOptions } from "./types.js";
|
|
22
18
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
const NAMESPACE = "translation";
|
|
20
|
+
const FALLBACK_LANGUAGE = i18nSharedOptions.fallbackLng;
|
|
21
|
+
const STORAGE_KEY = "i18nextLng";
|
|
22
|
+
const COOKIE_KEY = "i18next";
|
|
23
|
+
const COOKIE_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How long to wait for a catalogue before rendering untranslated.
|
|
27
|
+
*
|
|
28
|
+
* A widget with no text is worse than a widget in English: every caller
|
|
29
|
+
* supplies a `defaultValue`, and `t()` falls back to the key otherwise, so a
|
|
30
|
+
* catalogue that never arrives degrades rather than blocks.
|
|
31
|
+
*/
|
|
32
|
+
const CATALOGUE_FETCH_TIMEOUT_MS = 10_000;
|
|
33
|
+
|
|
34
|
+
type Catalogue = Record<string, string>;
|
|
35
|
+
|
|
36
|
+
const SUPPORTED_LANGUAGES: readonly string[] = i18nSharedOptions.supportedLngs;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Locale JSON sits next to the bundle that asked for it. The widget is embedded
|
|
40
|
+
* by third parties and cannot know a base URL, but it does know where its own
|
|
41
|
+
* module was served from.
|
|
42
|
+
*/
|
|
43
|
+
const catalogueUrl = (language: string): string => {
|
|
44
|
+
const moduleUrl = import.meta.url;
|
|
45
|
+
const directory = moduleUrl.slice(0, moduleUrl.lastIndexOf("/"));
|
|
46
|
+
return `${directory}/locales/${language}/${NAMESPACE}.json`;
|
|
31
47
|
};
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
/**
|
|
50
|
+
* `en-GB` is not in the catalogue list but `en` is, so a regional tag resolves
|
|
51
|
+
* to its base language rather than falling all the way back to English.
|
|
52
|
+
*/
|
|
53
|
+
const resolveLanguage = (candidate: string | undefined): string | undefined => {
|
|
54
|
+
if (!candidate) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (SUPPORTED_LANGUAGES.includes(candidate)) {
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
const base = candidate.split("-")[0];
|
|
61
|
+
return base && SUPPORTED_LANGUAGES.includes(base) ? base : undefined;
|
|
36
62
|
};
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
const readCookie = (): string | undefined => {
|
|
65
|
+
try {
|
|
66
|
+
const match = document.cookie.match(
|
|
67
|
+
new RegExp(`(?:^|; )${COOKIE_KEY}=([^;]*)`),
|
|
68
|
+
);
|
|
69
|
+
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
|
|
70
|
+
} catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const readStorage = (): string | undefined => {
|
|
76
|
+
try {
|
|
77
|
+
return localStorage.getItem(STORAGE_KEY) ?? undefined;
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const navigatorLanguages = (): readonly string[] => {
|
|
84
|
+
try {
|
|
85
|
+
return navigator.languages?.length
|
|
86
|
+
? navigator.languages
|
|
87
|
+
: [navigator.language];
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cookie and localStorage are both written, matching what the widget did when
|
|
95
|
+
* i18next-browser-languagedetector handled this, so a visitor's choice survives
|
|
96
|
+
* across pages that embed the widget. Either store can throw in a sandboxed
|
|
97
|
+
* iframe or with third-party storage blocked; neither is load-bearing.
|
|
98
|
+
*/
|
|
99
|
+
const cacheLanguage = (language: string): void => {
|
|
100
|
+
try {
|
|
101
|
+
localStorage.setItem(STORAGE_KEY, language);
|
|
102
|
+
} catch {
|
|
103
|
+
// storage unavailable — the language is still active for this page
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
document.cookie = `${COOKIE_KEY}=${encodeURIComponent(language)};max-age=${COOKIE_MAX_AGE_SECONDS};path=/`;
|
|
107
|
+
} catch {
|
|
108
|
+
// cookies unavailable — as above
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const detectLanguage = (): string => {
|
|
113
|
+
const candidates = [readCookie(), readStorage(), ...navigatorLanguages()];
|
|
114
|
+
for (const candidate of candidates) {
|
|
115
|
+
const resolved = resolveLanguage(candidate);
|
|
116
|
+
if (resolved) {
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return FALLBACK_LANGUAGE;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const fetchCatalogue = async (language: string): Promise<Catalogue> => {
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
const timer = setTimeout(
|
|
126
|
+
() => controller.abort(),
|
|
127
|
+
CATALOGUE_FETCH_TIMEOUT_MS,
|
|
128
|
+
);
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetch(catalogueUrl(language), {
|
|
131
|
+
signal: controller.signal,
|
|
132
|
+
});
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
const body: unknown = await response.json();
|
|
137
|
+
return typeof body === "object" && body !== null ? flatten(body) : {};
|
|
138
|
+
} catch {
|
|
139
|
+
return {};
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const interpolate = (template: string, options?: TranslateOptions): string =>
|
|
146
|
+
options
|
|
147
|
+
? template.replace(/\{\{(\w+)\}\}/g, (placeholder, name: string) =>
|
|
148
|
+
name in options ? String(options[name]) : placeholder,
|
|
54
149
|
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
150
|
+
: template;
|
|
151
|
+
|
|
152
|
+
interface BrowserI18n extends Ti18n {
|
|
153
|
+
/** Resolves once the active language's catalogue has been fetched. */
|
|
154
|
+
ready(): Promise<BrowserI18n>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const createBrowserI18n = (requested?: string): BrowserI18n => {
|
|
158
|
+
const catalogues = new Map<string, Catalogue>();
|
|
159
|
+
const listeners = new Map<I18nEvent, Set<() => void>>();
|
|
160
|
+
let language = resolveLanguage(requested) ?? detectLanguage();
|
|
161
|
+
let initialised = false;
|
|
162
|
+
|
|
163
|
+
const emit = (event: I18nEvent): void => {
|
|
164
|
+
for (const listener of listeners.get(event) ?? []) {
|
|
165
|
+
listener();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The fallback catalogue is fetched alongside the active one so a key
|
|
171
|
+
* missing from a translation still renders English rather than its key —
|
|
172
|
+
* the same key-level fallback i18next gave us via `fallbackLng`.
|
|
173
|
+
*/
|
|
174
|
+
const loadCatalogues = async (target: string): Promise<void> => {
|
|
175
|
+
const wanted = [...new Set([target, FALLBACK_LANGUAGE])].filter(
|
|
176
|
+
(candidate) => !catalogues.has(candidate),
|
|
177
|
+
);
|
|
178
|
+
await Promise.all(
|
|
179
|
+
wanted.map(async (candidate) => {
|
|
180
|
+
catalogues.set(candidate, await fetchCatalogue(candidate));
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const lookup = (candidate: string, key: string): string | undefined =>
|
|
186
|
+
catalogues.get(candidate)?.[key];
|
|
187
|
+
|
|
188
|
+
cacheLanguage(language);
|
|
189
|
+
const loading = loadCatalogues(language).then(() => {
|
|
190
|
+
initialised = true;
|
|
191
|
+
emit("initialized");
|
|
192
|
+
emit("loaded");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const instance: BrowserI18n = {
|
|
196
|
+
get language(): string {
|
|
197
|
+
return language;
|
|
198
|
+
},
|
|
199
|
+
get isInitialized(): boolean {
|
|
200
|
+
return initialised;
|
|
201
|
+
},
|
|
202
|
+
t: (key: string, options?: TranslateOptions): string =>
|
|
203
|
+
interpolate(
|
|
204
|
+
lookup(language, key) ??
|
|
205
|
+
lookup(FALLBACK_LANGUAGE, key) ??
|
|
206
|
+
options?.defaultValue ??
|
|
207
|
+
key,
|
|
208
|
+
options,
|
|
209
|
+
),
|
|
210
|
+
hasLoadedNamespace: (namespace: string): boolean =>
|
|
211
|
+
namespace === NAMESPACE && catalogues.has(language),
|
|
212
|
+
changeLanguage: async (next: string): Promise<BrowserI18n> => {
|
|
213
|
+
const resolved = resolveLanguage(next) ?? FALLBACK_LANGUAGE;
|
|
214
|
+
if (resolved !== language) {
|
|
215
|
+
language = resolved;
|
|
216
|
+
cacheLanguage(resolved);
|
|
217
|
+
await loadCatalogues(resolved);
|
|
218
|
+
emit("loaded");
|
|
219
|
+
emit("languageChanged");
|
|
90
220
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
221
|
+
return instance;
|
|
222
|
+
},
|
|
223
|
+
on: (event: I18nEvent, listener: () => void): void => {
|
|
224
|
+
const existing = listeners.get(event) ?? new Set<() => void>();
|
|
225
|
+
existing.add(listener);
|
|
226
|
+
listeners.set(event, existing);
|
|
227
|
+
},
|
|
228
|
+
off: (event: I18nEvent, listener: () => void): void => {
|
|
229
|
+
listeners.get(event)?.delete(listener);
|
|
230
|
+
},
|
|
231
|
+
ready: async (): Promise<BrowserI18n> => {
|
|
232
|
+
await loading;
|
|
233
|
+
return instance;
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return instance;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The widget owns this instance. A host page running its own i18n must not be
|
|
242
|
+
* able to capture the widget's translations, and every widget on the page
|
|
243
|
+
* shares one catalogue fetch.
|
|
244
|
+
*/
|
|
245
|
+
let instance: BrowserI18n | undefined;
|
|
246
|
+
|
|
247
|
+
export function initializeI18n(
|
|
248
|
+
i18nLoadedCallback?: (value: Ti18n) => void,
|
|
249
|
+
lng?: string,
|
|
250
|
+
): Ti18n {
|
|
251
|
+
if (!instance) {
|
|
252
|
+
// Passing `lng` here rather than calling changeLanguage() afterwards is
|
|
253
|
+
// what stops the widget rendering a flash of the browser's language
|
|
254
|
+
// before the site owner's choice is applied.
|
|
255
|
+
const created = createBrowserI18n(lng);
|
|
256
|
+
instance = created;
|
|
257
|
+
void created.ready().then(() => i18nLoadedCallback?.(created));
|
|
94
258
|
} else {
|
|
95
|
-
// Already
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
// correct; the caller reconciles the language afterwards.
|
|
100
|
-
i18nLoadedCallback?.(i18n);
|
|
259
|
+
// Already built (a second widget on the page, or createTranslator() got
|
|
260
|
+
// here first). The callback is only armed in the branch above, so it has
|
|
261
|
+
// to fire here or loadI18next() never settles.
|
|
262
|
+
i18nLoadedCallback?.(instance);
|
|
101
263
|
}
|
|
102
|
-
return
|
|
264
|
+
return instance;
|
|
103
265
|
}
|
|
104
266
|
|
|
105
267
|
export default initializeI18n;
|
package/src/i18nMiddleware.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
+
import i18n from "i18next";
|
|
15
16
|
import { handle } from "i18next-http-middleware";
|
|
16
17
|
import loadI18next from "./loadI18next.js";
|
|
17
18
|
|
|
@@ -23,7 +24,11 @@ type HandleOptions = NonNullable<Parameters<typeof handle>[1]>;
|
|
|
23
24
|
async function i18nMiddleware(
|
|
24
25
|
options: HandleOptions,
|
|
25
26
|
): Promise<ReturnType<typeof handle>> {
|
|
26
|
-
|
|
27
|
+
// The middleware needs the i18next instance itself, not the narrow Ti18n
|
|
28
|
+
// the widget shares with it. loadI18next(true) initialises that very
|
|
29
|
+
// singleton and waits for its resources, so importing it here is the same
|
|
30
|
+
// object, fully loaded.
|
|
31
|
+
await loadI18next(true);
|
|
27
32
|
return handle(i18n, { ...options });
|
|
28
33
|
}
|
|
29
34
|
|
package/src/index.ts
CHANGED
|
@@ -14,9 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
export { default as i18nMiddleware } from "./i18nMiddleware.js";
|
|
16
16
|
export { default as loadI18next } from "./loadI18next.js";
|
|
17
|
-
export {
|
|
17
|
+
export { isLanguage, LanguageCodes, Languages } from "./translations.js";
|
|
18
|
+
export type { Language } from "./translations.js";
|
|
18
19
|
export { isClientSide } from "./util.js";
|
|
19
|
-
export {
|
|
20
|
+
export { translationKeys } from "./translationKey.js";
|
|
20
21
|
export type { TranslationKey } from "./translationKey.js";
|
|
21
|
-
export {
|
|
22
|
-
export type {
|
|
22
|
+
export { createTranslator } from "./translator.js";
|
|
23
|
+
export type { Translator } from "./translator.js";
|
|
24
|
+
export type {
|
|
25
|
+
I18nEvent,
|
|
26
|
+
Ti18n,
|
|
27
|
+
TranslateFn,
|
|
28
|
+
TranslateOptions,
|
|
29
|
+
} from "./types.js";
|
package/src/loadI18next.ts
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import type {
|
|
16
|
-
|
|
15
|
+
import type { Ti18n } from "./types.js";
|
|
16
|
+
|
|
17
|
+
let i18nInstance: Ti18n;
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* How long to wait for i18next to report `loaded` before giving up on it.
|
|
@@ -28,16 +29,16 @@ let i18nInstance: i18n;
|
|
|
28
29
|
export const I18N_LOAD_TIMEOUT_MS = 10_000;
|
|
29
30
|
|
|
30
31
|
const reconcileLanguage = async (
|
|
31
|
-
instance:
|
|
32
|
+
instance: Ti18n,
|
|
32
33
|
lng: string | undefined,
|
|
33
|
-
): Promise<
|
|
34
|
+
): Promise<Ti18n> => {
|
|
34
35
|
if (lng && instance.language !== lng) {
|
|
35
36
|
await instance.changeLanguage(lng);
|
|
36
37
|
}
|
|
37
38
|
return instance;
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
async function loadI18next(backend: boolean, lng?: string): Promise<
|
|
41
|
+
async function loadI18next(backend: boolean, lng?: string): Promise<Ti18n> {
|
|
41
42
|
return new Promise((resolve, reject) => {
|
|
42
43
|
// On timeout we resolve with the (possibly not fully loaded) instance
|
|
43
44
|
// rather than rejecting. i18next falls back to the key itself when a
|
|
@@ -69,7 +70,7 @@ async function loadI18next(backend: boolean, lng?: string): Promise<i18n> {
|
|
|
69
70
|
// Both latches are one-shot: i18next can emit `loaded` more than once
|
|
70
71
|
// (per namespace, and again after changeLanguage), and a late event
|
|
71
72
|
// arriving after the timeout must not re-settle or leak a second timer.
|
|
72
|
-
const done = (instance:
|
|
73
|
+
const done = (instance: Ti18n): void => {
|
|
73
74
|
if (settled) {
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
@@ -118,7 +119,7 @@ async function loadI18next(backend: boolean, lng?: string): Promise<i18n> {
|
|
|
118
119
|
}, lng);
|
|
119
120
|
} else {
|
|
120
121
|
// Singleton already exists (e.g. a prior widget mounted with a
|
|
121
|
-
// different language, or a
|
|
122
|
+
// different language, or a direct consumer initialised earlier).
|
|
122
123
|
// Reconcile before resolving so callers can render synchronously
|
|
123
124
|
// against the requested language instead of seeing a flash of
|
|
124
125
|
// the previous one.
|
|
@@ -132,6 +133,4 @@ async function loadI18next(backend: boolean, lng?: string): Promise<i18n> {
|
|
|
132
133
|
});
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
export type { i18n as Ti18n };
|
|
136
|
-
|
|
137
136
|
export default loadI18next;
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "كثرة الطلبات من عنوان البريد الإلكتروني هذا",
|
|
48
48
|
"VPN_BLOCKED": "لا يُسمح بالطلبات من اتصالات VPN لهذا الموقع",
|
|
49
49
|
"PROXY_BLOCKED": "لا يُسمح بالطلبات من خوادم البروكسي لهذا الموقع",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "عدد كبير جدًا من طلبات CAPTCHA من localhost لمفتاح الموقع هذا",
|
|
50
51
|
"TOR_BLOCKED": "لا يُسمح بالطلبات من عقد خروج Tor لهذا الموقع",
|
|
51
52
|
"ABUSER_BLOCKED": "لا يُسمح بالطلبات من الشبكات المُبلَّغ عنها بسبب إساءة الاستخدام لهذا الموقع",
|
|
52
53
|
"DATACENTER_BLOCKED": "لا يُسمح بالطلبات من عناوين IP لمراكز البيانات لهذا الموقع",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Bu e-poçt ünvanından çox sayda göndəriş",
|
|
48
48
|
"VPN_BLOCKED": "Bu sayt üçün VPN bağlantılarından sorğulara icazə verilmir",
|
|
49
49
|
"PROXY_BLOCKED": "Bu sayt üçün proksi serverlərdən sorğulara icazə verilmir",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Bu sayt açarı üçün localhost-dan həddindən artıq çox CAPTCHA sorğusu",
|
|
50
51
|
"TOR_BLOCKED": "Bu sayt üçün Tor çıxış qovşaqlarından sorğulara icazə verilmir",
|
|
51
52
|
"ABUSER_BLOCKED": "Bu sayt üçün sui-istifadə üçün işarələnmiş şəbəkələrdən sorğulara icazə verilmir",
|
|
52
53
|
"DATACENTER_BLOCKED": "Bu sayt üçün məlumat mərkəzi IP ünvanlarından sorğulara icazə verilmir",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Příliš mnoho odeslání z této e-mailové adresy",
|
|
48
48
|
"VPN_BLOCKED": "Požadavky z VPN připojení nejsou pro tento web povoleny",
|
|
49
49
|
"PROXY_BLOCKED": "Požadavky z proxy serverů nejsou pro tento web povoleny",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Příliš mnoho požadavků CAPTCHA z localhost pro tento klíč webu",
|
|
50
51
|
"TOR_BLOCKED": "Požadavky z výstupních uzlů Tor nejsou pro tento web povoleny",
|
|
51
52
|
"ABUSER_BLOCKED": "Požadavky ze sítí označených za zneužívající nejsou pro tento web povoleny",
|
|
52
53
|
"DATACENTER_BLOCKED": "Požadavky z IP adres datových center nejsou pro tento web povoleny",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Zu viele Einsendungen von dieser E-Mail-Adresse",
|
|
48
48
|
"VPN_BLOCKED": "Anfragen über VPN-Verbindungen sind für diese Website nicht erlaubt",
|
|
49
49
|
"PROXY_BLOCKED": "Anfragen über Proxy-Server sind für diese Website nicht erlaubt",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Zu viele CAPTCHA-Anfragen von localhost für diesen Site-Key",
|
|
50
51
|
"TOR_BLOCKED": "Anfragen über Tor-Exit-Knoten sind für diese Website nicht erlaubt",
|
|
51
52
|
"ABUSER_BLOCKED": "Anfragen von als missbräuchlich markierten Netzwerken sind für diese Website nicht erlaubt",
|
|
52
53
|
"DATACENTER_BLOCKED": "Anfragen von Rechenzentrum-IP-Adressen sind für diese Website nicht erlaubt",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Πάρα πολλές υποβολές από αυτή τη διεύθυνση email",
|
|
35
35
|
"VPN_BLOCKED": "Αιτήματα από συνδέσεις VPN δεν επιτρέπονται για αυτόν τον ιστότοπο",
|
|
36
36
|
"PROXY_BLOCKED": "Αιτήματα από διακομιστές proxy δεν επιτρέπονται για αυτόν τον ιστότοπο",
|
|
37
|
+
"TOO_MANY_LOCALHOST": "Πάρα πολλά αιτήματα CAPTCHA από το localhost για αυτό το κλειδί ιστότοπου",
|
|
37
38
|
"TOR_BLOCKED": "Αιτήματα από κόμβους εξόδου Tor δεν επιτρέπονται για αυτόν τον ιστότοπο",
|
|
38
39
|
"ABUSER_BLOCKED": "Αιτήματα από δίκτυα που έχουν επισημανθεί για κατάχρηση δεν επιτρέπονται για αυτόν τον ιστότοπο",
|
|
39
40
|
"DATACENTER_BLOCKED": "Αιτήματα από διευθύνσεις IP κέντρων δεδομένων δεν επιτρέπονται για αυτόν τον ιστότοπο",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Too many submissions from this email address",
|
|
140
140
|
"VPN_BLOCKED": "Requests from VPN connections are not allowed for this site",
|
|
141
141
|
"PROXY_BLOCKED": "Requests from proxy servers are not allowed for this site",
|
|
142
|
+
"TOO_MANY_LOCALHOST": "Too many CAPTCHA requests from localhost for this site key",
|
|
142
143
|
"TOR_BLOCKED": "Requests from Tor exit nodes are not allowed for this site",
|
|
143
144
|
"ABUSER_BLOCKED": "Requests from networks flagged for abuse are not allowed for this site",
|
|
144
145
|
"DATACENTER_BLOCKED": "Requests from datacenter IP addresses are not allowed for this site",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Demasiados envíos desde esta dirección de correo electrónico",
|
|
48
48
|
"VPN_BLOCKED": "Las solicitudes desde conexiones VPN no están permitidas para este sitio",
|
|
49
49
|
"PROXY_BLOCKED": "Las solicitudes desde servidores proxy no están permitidas para este sitio",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Demasiadas solicitudes de CAPTCHA desde localhost para esta clave de sitio",
|
|
50
51
|
"TOR_BLOCKED": "Las solicitudes desde nodos de salida Tor no están permitidas para este sitio",
|
|
51
52
|
"ABUSER_BLOCKED": "Las solicitudes desde redes marcadas por abuso no están permitidas para este sitio",
|
|
52
53
|
"DATACENTER_BLOCKED": "Las solicitudes desde direcciones IP de centros de datos no están permitidas para este sitio",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Liian monta lähetystä tästä sähköpostiosoitteesta",
|
|
54
54
|
"VPN_BLOCKED": "VPN-yhteyksien pyynnöt eivät ole sallittuja tälle sivustolle",
|
|
55
55
|
"PROXY_BLOCKED": "Välityspalvelimien pyynnöt eivät ole sallittuja tälle sivustolle",
|
|
56
|
+
"TOO_MANY_LOCALHOST": "Liian monta CAPTCHA-pyyntöä localhostista tälle sivustoavaimelle",
|
|
56
57
|
"TOR_BLOCKED": "Tor-poistumissolmujen pyynnöt eivät ole sallittuja tälle sivustolle",
|
|
57
58
|
"ABUSER_BLOCKED": "Väärinkäytöstä merkittyjen verkkojen pyynnöt eivät ole sallittuja tälle sivustolle",
|
|
58
59
|
"DATACENTER_BLOCKED": "Datakeskusten IP-osoitteiden pyynnöt eivät ole sallittuja tälle sivustolle",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Trop de soumissions depuis cette adresse e-mail",
|
|
48
48
|
"VPN_BLOCKED": "Les requêtes provenant de connexions VPN ne sont pas autorisées pour ce site",
|
|
49
49
|
"PROXY_BLOCKED": "Les requêtes provenant de serveurs proxy ne sont pas autorisées pour ce site",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Trop de requêtes CAPTCHA provenant de localhost pour cette clé de site",
|
|
50
51
|
"TOR_BLOCKED": "Les requêtes provenant de nœuds de sortie Tor ne sont pas autorisées pour ce site",
|
|
51
52
|
"ABUSER_BLOCKED": "Les requêtes provenant de réseaux signalés pour abus ne sont pas autorisées pour ce site",
|
|
52
53
|
"DATACENTER_BLOCKED": "Les requêtes provenant d'adresses IP de centres de données ne sont pas autorisées pour ce site",
|
|
@@ -346,6 +346,7 @@
|
|
|
346
346
|
"SPAM_EMAIL_COUNT_EXCEEDED": "इस ईमेल पते से बहुत अधिक सबमिशन",
|
|
347
347
|
"VPN_BLOCKED": "इस साइट के लिए VPN कनेक्शन से अनुरोध की अनुमति नहीं है",
|
|
348
348
|
"PROXY_BLOCKED": "इस साइट के लिए प्रॉक्सी सर्वर से अनुरोध की अनुमति नहीं है",
|
|
349
|
+
"TOO_MANY_LOCALHOST": "इस साइट कुंजी के लिए localhost से बहुत अधिक CAPTCHA अनुरोध",
|
|
349
350
|
"TOR_BLOCKED": "इस साइट के लिए Tor एक्ज़िट नोड्स से अनुरोध की अनुमति नहीं है",
|
|
350
351
|
"ABUSER_BLOCKED": "दुरुपयोग के लिए चिह्नित नेटवर्क से अनुरोध इस साइट के लिए अनुमत नहीं हैं",
|
|
351
352
|
"DATACENTER_BLOCKED": "इस साइट के लिए डेटासेंटर IP पतों से अनुरोध की अनुमति नहीं है",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Túl sok beküldés erről az e-mail címről",
|
|
48
48
|
"VPN_BLOCKED": "VPN-kapcsolatokról érkező kérések nem engedélyezettek ezen az oldalon",
|
|
49
49
|
"PROXY_BLOCKED": "Proxy szerverekről érkező kérések nem engedélyezettek ezen az oldalon",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Túl sok CAPTCHA-kérés a localhostról ehhez a webhelykulcshoz",
|
|
50
51
|
"TOR_BLOCKED": "Tor kilépési csomópontokról érkező kérések nem engedélyezettek ezen az oldalon",
|
|
51
52
|
"ABUSER_BLOCKED": "Visszaélésért megjelölt hálózatokról érkező kérések nem engedélyezettek ezen az oldalon",
|
|
52
53
|
"DATACENTER_BLOCKED": "Adatközponti IP-címekről érkező kérések nem engedélyezettek ezen az oldalon",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Terlalu banyak pengiriman dari alamat email ini",
|
|
48
48
|
"VPN_BLOCKED": "Permintaan dari koneksi VPN tidak diizinkan untuk situs ini",
|
|
49
49
|
"PROXY_BLOCKED": "Permintaan dari server proxy tidak diizinkan untuk situs ini",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Terlalu banyak permintaan CAPTCHA dari localhost untuk kunci situs ini",
|
|
50
51
|
"TOR_BLOCKED": "Permintaan dari node keluar Tor tidak diizinkan untuk situs ini",
|
|
51
52
|
"ABUSER_BLOCKED": "Permintaan dari jaringan yang ditandai karena penyalahgunaan tidak diizinkan untuk situs ini",
|
|
52
53
|
"DATACENTER_BLOCKED": "Permintaan dari alamat IP pusat data tidak diizinkan untuk situs ini",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"SPAM_EMAIL_COUNT_EXCEEDED": "Troppi invii da questo indirizzo email",
|
|
48
48
|
"VPN_BLOCKED": "Le richieste da connessioni VPN non sono consentite per questo sito",
|
|
49
49
|
"PROXY_BLOCKED": "Le richieste da server proxy non sono consentite per questo sito",
|
|
50
|
+
"TOO_MANY_LOCALHOST": "Troppe richieste CAPTCHA da localhost per questa chiave del sito",
|
|
50
51
|
"TOR_BLOCKED": "Le richieste da nodi di uscita Tor non sono consentite per questo sito",
|
|
51
52
|
"ABUSER_BLOCKED": "Le richieste da reti segnalate per abuso non sono consentite per questo sito",
|
|
52
53
|
"DATACENTER_BLOCKED": "Le richieste da indirizzi IP di data center non sono consentite per questo sito",
|