react-cheminfo 0.23.0 → 0.25.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/README.md +39 -26
- package/lib/chrome/ui/HeaderToggle.d.ts +52 -0
- package/lib/chrome/ui/HeaderToggle.d.ts.map +1 -0
- package/lib/chrome/ui/HeaderToggle.js +31 -0
- package/lib/chrome/ui/HeaderToggle.js.map +1 -0
- package/lib/chrome/ui/index.d.ts +2 -0
- package/lib/chrome/ui/index.d.ts.map +1 -1
- package/lib/chrome/ui/index.js +1 -0
- package/lib/chrome/ui/index.js.map +1 -1
- package/lib/citation/core/gfn2Paper.d.ts +10 -0
- package/lib/citation/core/gfn2Paper.d.ts.map +1 -0
- package/lib/citation/core/gfn2Paper.js +28 -0
- package/lib/citation/core/gfn2Paper.js.map +1 -0
- package/lib/citation/core/index.d.ts +1 -0
- package/lib/citation/core/index.d.ts.map +1 -1
- package/lib/citation/core/index.js +1 -0
- package/lib/citation/core/index.js.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.d.ts +8 -7
- package/lib/clipboard/ui/ClickToCopy.d.ts.map +1 -1
- package/lib/clipboard/ui/ClickToCopy.js +3 -3
- package/lib/clipboard/ui/ClickToCopy.js.map +1 -1
- package/lib/conformers.d.ts +27 -0
- package/lib/conformers.d.ts.map +1 -0
- package/lib/conformers.js +19 -0
- package/lib/conformers.js.map +1 -0
- package/lib/core.d.ts +1 -0
- package/lib/core.d.ts.map +1 -1
- package/lib/core.js +1 -0
- package/lib/core.js.map +1 -1
- package/lib/credits/core/credits.d.ts +12 -0
- package/lib/credits/core/credits.d.ts.map +1 -1
- package/lib/credits/core/credits.js +14 -0
- package/lib/credits/core/credits.js.map +1 -1
- package/lib/ecosystem/core/index.d.ts +1 -1
- package/lib/ecosystem/core/index.d.ts.map +1 -1
- package/lib/ecosystem/core/index.js.map +1 -1
- package/lib/ecosystem/core/sites.d.ts +11 -1
- package/lib/ecosystem/core/sites.d.ts.map +1 -1
- package/lib/ecosystem/core/sites.js +11 -4
- package/lib/ecosystem/core/sites.js.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.d.ts.map +1 -1
- package/lib/ecosystem/ui/EcosystemLinks.js +3 -1
- package/lib/ecosystem/ui/EcosystemLinks.js.map +1 -1
- package/lib/ecosystem/ui/SiteTile.d.ts.map +1 -1
- package/lib/ecosystem/ui/SiteTile.js +3 -1
- package/lib/ecosystem/ui/SiteTile.js.map +1 -1
- package/lib/ecosystem/ui/glyphs.d.ts.map +1 -1
- package/lib/ecosystem/ui/glyphs.js +6 -5
- package/lib/ecosystem/ui/glyphs.js.map +1 -1
- package/lib/format/core/index.d.ts +1 -1
- package/lib/format/core/index.d.ts.map +1 -1
- package/lib/format/core/index.js +1 -1
- package/lib/format/core/index.js.map +1 -1
- package/lib/format/core/words.d.ts +9 -0
- package/lib/format/core/words.d.ts.map +1 -1
- package/lib/format/core/words.js +16 -0
- package/lib/format/core/words.js.map +1 -1
- package/lib/language/core/index.d.ts +3 -0
- package/lib/language/core/index.d.ts.map +1 -0
- package/lib/language/core/index.js +3 -0
- package/lib/language/core/index.js.map +1 -0
- package/lib/language/core/languageName.d.ts +8 -0
- package/lib/language/core/languageName.d.ts.map +1 -0
- package/lib/language/core/languageName.js +16 -0
- package/lib/language/core/languageName.js.map +1 -0
- package/lib/language/core/languageParam.d.ts +35 -0
- package/lib/language/core/languageParam.d.ts.map +1 -0
- package/lib/language/core/languageParam.js +67 -0
- package/lib/language/core/languageParam.js.map +1 -0
- package/lib/language/ui/SiteLanguage.d.ts +20 -0
- package/lib/language/ui/SiteLanguage.d.ts.map +1 -0
- package/lib/language/ui/SiteLanguage.js +14 -0
- package/lib/language/ui/SiteLanguage.js.map +1 -0
- package/lib/language/ui/index.d.ts +4 -0
- package/lib/language/ui/index.d.ts.map +1 -0
- package/lib/language/ui/index.js +3 -0
- package/lib/language/ui/index.js.map +1 -0
- package/lib/language/ui/siteLanguageContext.d.ts +12 -0
- package/lib/language/ui/siteLanguageContext.d.ts.map +1 -0
- package/lib/language/ui/siteLanguageContext.js +19 -0
- package/lib/language/ui/siteLanguageContext.js.map +1 -0
- package/lib/orbital/core/atomicOrbitals.d.ts +9 -1
- package/lib/orbital/core/atomicOrbitals.d.ts.map +1 -1
- package/lib/orbital/core/atomicOrbitals.js +7 -0
- package/lib/orbital/core/atomicOrbitals.js.map +1 -1
- package/lib/orbital/core/hydrogenic.d.ts +2 -1
- package/lib/orbital/core/hydrogenic.d.ts.map +1 -1
- package/lib/orbital/core/hydrogenic.js +2 -1
- package/lib/orbital/core/hydrogenic.js.map +1 -1
- package/lib/orbital/core/index.d.ts +1 -0
- package/lib/orbital/core/index.d.ts.map +1 -1
- package/lib/orbital/core/index.js +1 -0
- package/lib/orbital/core/index.js.map +1 -1
- package/lib/orbital/core/slaterEnergy.d.ts +35 -0
- package/lib/orbital/core/slaterEnergy.d.ts.map +1 -0
- package/lib/orbital/core/slaterEnergy.js +66 -0
- package/lib/orbital/core/slaterEnergy.js.map +1 -0
- package/lib/shared/ui/MenuButton.d.ts +3 -2
- package/lib/shared/ui/MenuButton.d.ts.map +1 -1
- package/lib/shared/ui/MenuButton.js +1 -1
- package/lib/shared/ui/MenuButton.js.map +1 -1
- package/lib/structure/core/conformerMinimise.d.ts +36 -0
- package/lib/structure/core/conformerMinimise.d.ts.map +1 -0
- package/lib/structure/core/conformerMinimise.js +81 -0
- package/lib/structure/core/conformerMinimise.js.map +1 -0
- package/lib/structure/core/conformerMinimum.d.ts +62 -0
- package/lib/structure/core/conformerMinimum.d.ts.map +1 -0
- package/lib/structure/core/conformerMinimum.js +90 -0
- package/lib/structure/core/conformerMinimum.js.map +1 -0
- package/lib/structure/core/conformerOptions.d.ts +62 -0
- package/lib/structure/core/conformerOptions.d.ts.map +1 -0
- package/lib/structure/core/conformerOptions.js +83 -0
- package/lib/structure/core/conformerOptions.js.map +1 -0
- package/lib/structure/core/conformerRefine.d.ts +104 -0
- package/lib/structure/core/conformerRefine.d.ts.map +1 -0
- package/lib/structure/core/conformerRefine.js +130 -0
- package/lib/structure/core/conformerRefine.js.map +1 -0
- package/lib/structure/core/conformerRefineRank.d.ts +36 -0
- package/lib/structure/core/conformerRefineRank.d.ts.map +1 -0
- package/lib/structure/core/conformerRefineRank.js +69 -0
- package/lib/structure/core/conformerRefineRank.js.map +1 -0
- package/lib/structure/core/conformerSession.d.ts +50 -0
- package/lib/structure/core/conformerSession.d.ts.map +1 -0
- package/lib/structure/core/conformerSession.js +88 -0
- package/lib/structure/core/conformerSession.js.map +1 -0
- package/lib/structure/core/conformerShape.d.ts +36 -0
- package/lib/structure/core/conformerShape.d.ts.map +1 -0
- package/lib/structure/core/conformerShape.js +150 -0
- package/lib/structure/core/conformerShape.js.map +1 -0
- package/lib/structure/core/conformers.d.ts +107 -0
- package/lib/structure/core/conformers.d.ts.map +1 -0
- package/lib/structure/core/conformers.js +149 -0
- package/lib/structure/core/conformers.js.map +1 -0
- package/lib/structure/core/geometryRelaxer.d.ts +66 -0
- package/lib/structure/core/geometryRelaxer.d.ts.map +1 -0
- package/lib/structure/core/geometryRelaxer.js +2 -0
- package/lib/structure/core/geometryRelaxer.js.map +1 -0
- package/lib/structure/core/index.d.ts +4 -0
- package/lib/structure/core/index.d.ts.map +1 -1
- package/lib/structure/core/index.js +2 -0
- package/lib/structure/core/index.js.map +1 -1
- package/lib/structure/core/moleculeCoordinates.d.ts +31 -0
- package/lib/structure/core/moleculeCoordinates.d.ts.map +1 -0
- package/lib/structure/core/moleculeCoordinates.js +80 -0
- package/lib/structure/core/moleculeCoordinates.js.map +1 -0
- package/lib/structure/core/molfileExport.d.ts +42 -0
- package/lib/structure/core/molfileExport.d.ts.map +1 -0
- package/lib/structure/core/molfileExport.js +47 -0
- package/lib/structure/core/molfileExport.js.map +1 -0
- package/lib/structure/core/oclResources.d.ts +12 -0
- package/lib/structure/core/oclResources.d.ts.map +1 -0
- package/lib/structure/core/oclResources.js +35 -0
- package/lib/structure/core/oclResources.js.map +1 -0
- package/lib/translate/core/catalog.d.ts +61 -0
- package/lib/translate/core/catalog.d.ts.map +1 -0
- package/lib/translate/core/catalog.js +49 -0
- package/lib/translate/core/catalog.js.map +1 -0
- package/lib/translate/core/checkTranslation.d.ts +32 -0
- package/lib/translate/core/checkTranslation.d.ts.map +1 -0
- package/lib/translate/core/checkTranslation.js +98 -0
- package/lib/translate/core/checkTranslation.js.map +1 -0
- package/lib/translate/core/contribution.d.ts +88 -0
- package/lib/translate/core/contribution.d.ts.map +1 -0
- package/lib/translate/core/contribution.js +14 -0
- package/lib/translate/core/contribution.js.map +1 -0
- package/lib/translate/core/index.d.ts +19 -0
- package/lib/translate/core/index.d.ts.map +1 -0
- package/lib/translate/core/index.js +15 -0
- package/lib/translate/core/index.js.map +1 -0
- package/lib/translate/core/marker.d.ts +45 -0
- package/lib/translate/core/marker.d.ts.map +1 -0
- package/lib/translate/core/marker.js +79 -0
- package/lib/translate/core/marker.js.map +1 -0
- package/lib/translate/core/mergeMessages.d.ts +35 -0
- package/lib/translate/core/mergeMessages.d.ts.map +1 -0
- package/lib/translate/core/mergeMessages.js +69 -0
- package/lib/translate/core/mergeMessages.js.map +1 -0
- package/lib/translate/core/session.d.ts +102 -0
- package/lib/translate/core/session.d.ts.map +1 -0
- package/lib/translate/core/session.js +162 -0
- package/lib/translate/core/session.js.map +1 -0
- package/lib/translate/core/start.d.ts +103 -0
- package/lib/translate/core/start.d.ts.map +1 -0
- package/lib/translate/core/start.js +92 -0
- package/lib/translate/core/start.js.map +1 -0
- package/lib/translate/core/suggestions.d.ts +77 -0
- package/lib/translate/core/suggestions.d.ts.map +1 -0
- package/lib/translate/core/suggestions.js +143 -0
- package/lib/translate/core/suggestions.js.map +1 -0
- package/lib/translate/core/tables.d.ts +100 -0
- package/lib/translate/core/tables.d.ts.map +1 -0
- package/lib/translate/core/tables.js +51 -0
- package/lib/translate/core/tables.js.map +1 -0
- package/lib/translate.d.ts +2 -0
- package/lib/translate.d.ts.map +1 -0
- package/lib/translate.js +6 -0
- package/lib/translate.js.map +1 -0
- package/lib/ui.d.ts +1 -0
- package/lib/ui.d.ts.map +1 -1
- package/lib/ui.js +1 -0
- package/lib/ui.js.map +1 -1
- package/lib/xtb/core/xtbRelaxer.d.ts +61 -0
- package/lib/xtb/core/xtbRelaxer.d.ts.map +1 -0
- package/lib/xtb/core/xtbRelaxer.js +108 -0
- package/lib/xtb/core/xtbRelaxer.js.map +1 -0
- package/lib/xtb.d.ts +11 -0
- package/lib/xtb.d.ts.map +1 -0
- package/lib/xtb.js +10 -0
- package/lib/xtb.js.map +1 -0
- package/package.json +18 -11
- package/src/chrome/ui/HeaderToggle.tsx +100 -0
- package/src/chrome/ui/index.ts +2 -0
- package/src/citation/core/gfn2Paper.ts +32 -0
- package/src/citation/core/index.ts +1 -0
- package/src/clipboard/ui/ClickToCopy.tsx +8 -7
- package/src/conformers.ts +87 -0
- package/src/core.ts +1 -0
- package/src/credits/core/credits.ts +16 -0
- package/src/ecosystem/core/index.ts +1 -0
- package/src/ecosystem/core/sites.ts +24 -4
- package/src/ecosystem/ui/EcosystemLinks.tsx +3 -1
- package/src/ecosystem/ui/SiteTile.tsx +3 -1
- package/src/ecosystem/ui/glyphs.tsx +16 -22
- package/src/format/core/index.ts +1 -1
- package/src/format/core/words.ts +19 -0
- package/src/language/core/index.ts +7 -0
- package/src/language/core/languageName.ts +16 -0
- package/src/language/core/languageParam.ts +70 -0
- package/src/language/ui/SiteLanguage.tsx +26 -0
- package/src/language/ui/index.ts +3 -0
- package/src/language/ui/siteLanguageContext.ts +20 -0
- package/src/orbital/core/atomicOrbitals.ts +17 -1
- package/src/orbital/core/hydrogenic.ts +2 -1
- package/src/orbital/core/index.ts +1 -0
- package/src/orbital/core/slaterEnergy.ts +83 -0
- package/src/shared/ui/MenuButton.tsx +4 -3
- package/src/structure/core/conformerMinimise.ts +99 -0
- package/src/structure/core/conformerMinimum.ts +125 -0
- package/src/structure/core/conformerOptions.ts +125 -0
- package/src/structure/core/conformerRefine.ts +234 -0
- package/src/structure/core/conformerRefineRank.ts +89 -0
- package/src/structure/core/conformerSession.ts +123 -0
- package/src/structure/core/conformerShape.ts +181 -0
- package/src/structure/core/conformers.ts +282 -0
- package/src/structure/core/geometryRelaxer.ts +71 -0
- package/src/structure/core/index.ts +13 -0
- package/src/structure/core/moleculeCoordinates.ts +93 -0
- package/src/structure/core/molfileExport.ts +62 -0
- package/src/structure/core/oclResources.ts +41 -0
- package/src/translate/core/catalog.ts +83 -0
- package/src/translate/core/checkTranslation.ts +128 -0
- package/src/translate/core/contribution.ts +99 -0
- package/src/translate/core/index.ts +72 -0
- package/src/translate/core/marker.ts +85 -0
- package/src/translate/core/mergeMessages.ts +74 -0
- package/src/translate/core/session.ts +250 -0
- package/src/translate/core/start.ts +183 -0
- package/src/translate/core/suggestions.ts +211 -0
- package/src/translate/core/tables.ts +120 -0
- package/src/translate.ts +5 -0
- package/src/ui.ts +1 -0
- package/src/xtb/core/xtbRelaxer.ts +160 -0
- package/src/xtb.ts +16 -0
- package/styles/chrome.css +61 -30
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Messages } from './catalog.ts';
|
|
2
|
+
import { ownMessage } from './catalog.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge a translator's messages into a locale file.
|
|
6
|
+
*
|
|
7
|
+
* Keys come out in the order of the English file, so a pull request's diff is
|
|
8
|
+
* the lines that changed and nothing else; keys the English file no longer has
|
|
9
|
+
* are kept, last, because removing them is not the translator's decision.
|
|
10
|
+
* @param source - The English messages, whose order is followed.
|
|
11
|
+
* @param existing - The locale file as it is in the repository.
|
|
12
|
+
* @param updates - The messages the translator wrote.
|
|
13
|
+
* @returns The merged messages.
|
|
14
|
+
*/
|
|
15
|
+
export function mergeMessages(
|
|
16
|
+
source: Messages,
|
|
17
|
+
existing: Messages,
|
|
18
|
+
updates: Messages,
|
|
19
|
+
): Messages {
|
|
20
|
+
const entries: Array<[string, string]> = [];
|
|
21
|
+
for (const key of Object.keys(source)) {
|
|
22
|
+
const message = ownMessage(updates, key) ?? ownMessage(existing, key);
|
|
23
|
+
if (message !== undefined) entries.push([key, message]);
|
|
24
|
+
}
|
|
25
|
+
for (const [key, message] of Object.entries(existing)) {
|
|
26
|
+
if (!Object.hasOwn(source, key)) entries.push([key, message]);
|
|
27
|
+
}
|
|
28
|
+
return Object.fromEntries(entries);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The keys whose message an update actually changes.
|
|
33
|
+
* @param existing - The locale file as it is in the repository.
|
|
34
|
+
* @param updates - The messages the translator wrote.
|
|
35
|
+
* @returns The keys that differ, in the order of the updates.
|
|
36
|
+
*/
|
|
37
|
+
export function changedKeys(existing: Messages, updates: Messages): string[] {
|
|
38
|
+
const keys: string[] = [];
|
|
39
|
+
for (const [key, message] of Object.entries(updates)) {
|
|
40
|
+
if (ownMessage(existing, key) !== message) keys.push(key);
|
|
41
|
+
}
|
|
42
|
+
return keys;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write messages the way a catalog file is committed: two-space JSON with a
|
|
47
|
+
* final newline, which is also what Prettier leaves untouched.
|
|
48
|
+
* @param messages - The messages.
|
|
49
|
+
* @returns The file content.
|
|
50
|
+
*/
|
|
51
|
+
export function serializeMessages(messages: Messages): string {
|
|
52
|
+
return `${JSON.stringify(messages, null, 2)}\n`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read a catalog file, refusing anything but a flat object of strings.
|
|
57
|
+
* @param text - The file content.
|
|
58
|
+
* @returns The messages.
|
|
59
|
+
* @throws {TypeError} When the content is not a flat object of strings.
|
|
60
|
+
*/
|
|
61
|
+
export function parseMessages(text: string): Messages {
|
|
62
|
+
const parsed: unknown = JSON.parse(text);
|
|
63
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
64
|
+
throw new TypeError('a catalog file must hold one JSON object');
|
|
65
|
+
}
|
|
66
|
+
const entries: Array<[string, string]> = [];
|
|
67
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
68
|
+
if (typeof value !== 'string') {
|
|
69
|
+
throw new TypeError(`the message "${key}" is not a string`);
|
|
70
|
+
}
|
|
71
|
+
entries.push([key, value]);
|
|
72
|
+
}
|
|
73
|
+
return Object.fromEntries(entries);
|
|
74
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { IntlMessageFormat } from 'intl-messageformat';
|
|
2
|
+
|
|
3
|
+
import type { CatalogSource, MessageRef, Messages } from './catalog.ts';
|
|
4
|
+
import { SOURCE_LOCALE, ownMessage } from './catalog.ts';
|
|
5
|
+
import { checkTranslation } from './checkTranslation.ts';
|
|
6
|
+
import { markText } from './marker.ts';
|
|
7
|
+
import type { TranslatableTable } from './tables.ts';
|
|
8
|
+
|
|
9
|
+
/** The values a message's placeholders are filled with. */
|
|
10
|
+
export type MessageValues = Record<string, string | number | Date>;
|
|
11
|
+
|
|
12
|
+
/** The version of the page ↔ overlay protocol this session speaks. */
|
|
13
|
+
export const BRIDGE_PROTOCOL = 2;
|
|
14
|
+
|
|
15
|
+
/** Where a page in translate mode exposes its session to the overlay. */
|
|
16
|
+
export const BRIDGE_GLOBAL = '__cheminfoTranslate';
|
|
17
|
+
|
|
18
|
+
/** A catalog as the overlay sees it: the source, what is published, the edits. */
|
|
19
|
+
export interface CatalogSnapshot extends CatalogSource {
|
|
20
|
+
/** The locale file as published with the site. */
|
|
21
|
+
translation: Messages;
|
|
22
|
+
/** The messages edited in the overlay and not yet submitted. */
|
|
23
|
+
drafts: Messages;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What a page in translate mode offers the overlay. It is the only contract
|
|
28
|
+
* between the two, so an overlay released later still drives a page built
|
|
29
|
+
* earlier as long as the protocol number matches.
|
|
30
|
+
*/
|
|
31
|
+
export interface TranslateBridge {
|
|
32
|
+
/** The protocol version; the overlay refuses a page speaking another. */
|
|
33
|
+
readonly protocol: typeof BRIDGE_PROTOCOL;
|
|
34
|
+
/** The locale being translated into. */
|
|
35
|
+
readonly locale: string;
|
|
36
|
+
/** Every catalog the page registered. */
|
|
37
|
+
catalogs: () => CatalogSnapshot[];
|
|
38
|
+
/**
|
|
39
|
+
* The tables of the page whose rows a translator may write — the element
|
|
40
|
+
* sheet, a list of ions — with the columns that are text rather than
|
|
41
|
+
* measurement. Empty on a page that shows no table.
|
|
42
|
+
*/
|
|
43
|
+
tables: () => TranslatableTable[];
|
|
44
|
+
/** The message a marker found in the DOM stands for. */
|
|
45
|
+
resolveMarker: (id: number) => MessageRef | undefined;
|
|
46
|
+
/** Show an edited message on the page; `undefined` removes the edit. */
|
|
47
|
+
setDraft: (ref: MessageRef, message: string | undefined) => void;
|
|
48
|
+
/** Be told whenever a draft changes; returns the function that stops it. */
|
|
49
|
+
subscribe: (listener: () => void) => () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** How a session is set up. */
|
|
53
|
+
export interface TranslateSessionOptions {
|
|
54
|
+
/** The locale messages are shown in. */
|
|
55
|
+
locale: string;
|
|
56
|
+
/** Every catalog the page renders from. */
|
|
57
|
+
catalogs: readonly CatalogSource[];
|
|
58
|
+
/**
|
|
59
|
+
* The published messages of `locale`, by catalog id.
|
|
60
|
+
* @default {}
|
|
61
|
+
*/
|
|
62
|
+
translations?: Readonly<Record<string, Messages>>;
|
|
63
|
+
/**
|
|
64
|
+
* Whether every formatted message carries its invisible marker, which is
|
|
65
|
+
* what translate mode is.
|
|
66
|
+
* @default false
|
|
67
|
+
*/
|
|
68
|
+
marked?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The tables whose rows are translated, declared so the overlay can offer
|
|
71
|
+
* them as a grid: a cell of a table is rarely on the page being looked at,
|
|
72
|
+
* and 118 of them are never Alt-clicked one by one.
|
|
73
|
+
* @default []
|
|
74
|
+
*/
|
|
75
|
+
tables?: readonly TranslatableTable[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface CatalogState {
|
|
79
|
+
source: CatalogSource;
|
|
80
|
+
translation: Messages;
|
|
81
|
+
drafts: Map<string, string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Formats the messages of a page, and in translate mode lets the overlay edit
|
|
86
|
+
* them live: a draft replaces the published message as soon as it can be
|
|
87
|
+
* formatted, and a draft that cannot is ignored rather than breaking the page.
|
|
88
|
+
*/
|
|
89
|
+
export class TranslateSession implements TranslateBridge {
|
|
90
|
+
public readonly protocol = BRIDGE_PROTOCOL;
|
|
91
|
+
public readonly locale: string;
|
|
92
|
+
readonly #marked: boolean;
|
|
93
|
+
readonly #catalogs = new Map<string, CatalogState>();
|
|
94
|
+
readonly #ids = new Map<string, number>();
|
|
95
|
+
readonly #refs: MessageRef[] = [];
|
|
96
|
+
readonly #listeners = new Set<() => void>();
|
|
97
|
+
readonly #formatters = new Map<string, IntlMessageFormat | null>();
|
|
98
|
+
readonly #checked = new Map<string, boolean>();
|
|
99
|
+
readonly #tables: readonly TranslatableTable[];
|
|
100
|
+
#version = 0;
|
|
101
|
+
|
|
102
|
+
public constructor(options: TranslateSessionOptions) {
|
|
103
|
+
const {
|
|
104
|
+
locale,
|
|
105
|
+
catalogs,
|
|
106
|
+
translations = {},
|
|
107
|
+
marked = false,
|
|
108
|
+
tables = [],
|
|
109
|
+
} = options;
|
|
110
|
+
this.locale = locale;
|
|
111
|
+
this.#marked = marked;
|
|
112
|
+
this.#tables = tables;
|
|
113
|
+
for (const source of catalogs) {
|
|
114
|
+
this.#catalogs.set(source.id, {
|
|
115
|
+
source,
|
|
116
|
+
translation: translations[source.id] ?? {},
|
|
117
|
+
drafts: new Map(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Increases on every draft change, for a view that re-renders on it.
|
|
124
|
+
* @returns The current version.
|
|
125
|
+
*/
|
|
126
|
+
public get version(): number {
|
|
127
|
+
return this.#version;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Format one message: the draft, else the published translation, else the
|
|
132
|
+
* English message — the first that passes its check against the English
|
|
133
|
+
* one and formats. A draft that drops a placeholder formats without error,
|
|
134
|
+
* so the check, not the formatting, is what keeps it off the page.
|
|
135
|
+
* @param catalogId - The catalog the message belongs to.
|
|
136
|
+
* @param key - The key of the message.
|
|
137
|
+
* @param values - What the placeholders are filled with.
|
|
138
|
+
* @returns The text to show; the key itself when no catalog has it.
|
|
139
|
+
*/
|
|
140
|
+
public format(catalogId: string, key: string, values?: MessageValues) {
|
|
141
|
+
const catalog = this.#catalogs.get(catalogId);
|
|
142
|
+
if (catalog === undefined) return key;
|
|
143
|
+
const source = ownMessage(catalog.source.messages, key);
|
|
144
|
+
if (source === undefined) return key;
|
|
145
|
+
|
|
146
|
+
const draft = this.#usable(source, catalog.drafts.get(key));
|
|
147
|
+
const translation = this.#usable(
|
|
148
|
+
source,
|
|
149
|
+
ownMessage(catalog.translation, key),
|
|
150
|
+
);
|
|
151
|
+
const text =
|
|
152
|
+
this.#tryFormat(draft, this.locale, values) ??
|
|
153
|
+
this.#tryFormat(translation, this.locale, values) ??
|
|
154
|
+
this.#tryFormat(source, SOURCE_LOCALE, values) ??
|
|
155
|
+
source;
|
|
156
|
+
return this.#marked ? markText(text, this.#idOf(catalogId, key)) : text;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
public catalogs(): CatalogSnapshot[] {
|
|
160
|
+
const snapshots: CatalogSnapshot[] = [];
|
|
161
|
+
for (const { source, translation, drafts } of this.#catalogs.values()) {
|
|
162
|
+
snapshots.push({
|
|
163
|
+
...source,
|
|
164
|
+
translation,
|
|
165
|
+
drafts: Object.fromEntries(drafts),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return snapshots;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The tables whose rows a translator may write.
|
|
173
|
+
* @returns One entry per table the page declared.
|
|
174
|
+
*/
|
|
175
|
+
public tables(): TranslatableTable[] {
|
|
176
|
+
return [...this.#tables];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public resolveMarker(id: number): MessageRef | undefined {
|
|
180
|
+
return this.#refs[id];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public setDraft(ref: MessageRef, message: string | undefined): void {
|
|
184
|
+
const catalog = this.#catalogs.get(ref.catalogId);
|
|
185
|
+
if (catalog === undefined) return;
|
|
186
|
+
if (ownMessage(catalog.source.messages, ref.key) === undefined) return;
|
|
187
|
+
if (catalog.drafts.get(ref.key) === message) return;
|
|
188
|
+
if (message === undefined) {
|
|
189
|
+
catalog.drafts.delete(ref.key);
|
|
190
|
+
} else {
|
|
191
|
+
catalog.drafts.set(ref.key, message);
|
|
192
|
+
}
|
|
193
|
+
this.#version++;
|
|
194
|
+
for (const listener of this.#listeners) listener();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public subscribe(listener: () => void): () => void {
|
|
198
|
+
this.#listeners.add(listener);
|
|
199
|
+
return () => {
|
|
200
|
+
this.#listeners.delete(listener);
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#idOf(catalogId: string, key: string): number {
|
|
205
|
+
const name = `${catalogId}\n${key}`;
|
|
206
|
+
let id = this.#ids.get(name);
|
|
207
|
+
if (id === undefined) {
|
|
208
|
+
id = this.#refs.length;
|
|
209
|
+
this.#refs.push({ catalogId, key });
|
|
210
|
+
this.#ids.set(name, id);
|
|
211
|
+
}
|
|
212
|
+
return id;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#usable(source: string, message: string | undefined): string | undefined {
|
|
216
|
+
if (message === undefined) return undefined;
|
|
217
|
+
const name = JSON.stringify([source, message]);
|
|
218
|
+
let usable = this.#checked.get(name);
|
|
219
|
+
if (usable === undefined) {
|
|
220
|
+
usable = checkTranslation(source, message).length === 0;
|
|
221
|
+
this.#checked.set(name, usable);
|
|
222
|
+
}
|
|
223
|
+
return usable ? message : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#tryFormat(
|
|
227
|
+
message: string | undefined,
|
|
228
|
+
locale: string,
|
|
229
|
+
values: MessageValues | undefined,
|
|
230
|
+
): string | undefined {
|
|
231
|
+
if (message === undefined) return undefined;
|
|
232
|
+
const name = `${locale}\n${message}`;
|
|
233
|
+
let formatter = this.#formatters.get(name);
|
|
234
|
+
if (formatter === undefined) {
|
|
235
|
+
try {
|
|
236
|
+
formatter = new IntlMessageFormat(message, locale);
|
|
237
|
+
} catch {
|
|
238
|
+
formatter = null;
|
|
239
|
+
}
|
|
240
|
+
this.#formatters.set(name, formatter);
|
|
241
|
+
}
|
|
242
|
+
if (formatter === null) return undefined;
|
|
243
|
+
try {
|
|
244
|
+
const output = formatter.format(values);
|
|
245
|
+
return Array.isArray(output) ? output.join('') : String(output);
|
|
246
|
+
} catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a page into a translatable one.
|
|
3
|
+
*
|
|
4
|
+
* A site opened with `?translate=<locale>` formats every message with its
|
|
5
|
+
* invisible marker, hands its session to the overlay as
|
|
6
|
+
* `window.__cheminfoTranslate`, and loads that overlay from
|
|
7
|
+
* translate.cheminfo.org. Nothing of this is paid for by an ordinary visit:
|
|
8
|
+
* the site imports this module only once it has seen the parameter.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isLanguageTag } from '../../language/core/languageParam.ts';
|
|
12
|
+
|
|
13
|
+
import type { CatalogSource, Messages } from './catalog.ts';
|
|
14
|
+
import { SOURCE_LOCALE } from './catalog.ts';
|
|
15
|
+
import { BRIDGE_GLOBAL, TranslateSession } from './session.ts';
|
|
16
|
+
import type { TranslatableTable } from './tables.ts';
|
|
17
|
+
|
|
18
|
+
/** The query parameter that opens a page in translate mode. */
|
|
19
|
+
export const TRANSLATE_PARAM = 'translate';
|
|
20
|
+
|
|
21
|
+
/** Where the overlay is served from unless a site says otherwise. */
|
|
22
|
+
export const TRANSLATE_ORIGIN = 'https://translate.cheminfo.org';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The locale a page is being translated into, from its address:
|
|
26
|
+
* `?translate=fr` opens the overlay for French.
|
|
27
|
+
* @param search - The query string, e.g. `location.search`.
|
|
28
|
+
* @returns The locale, or `undefined` when the page is not in translate mode,
|
|
29
|
+
* or names English, or names something that is not a language at all.
|
|
30
|
+
*/
|
|
31
|
+
export function readTranslateLocale(search: string): string | undefined {
|
|
32
|
+
const value = new URLSearchParams(search).get(TRANSLATE_PARAM);
|
|
33
|
+
if (value === null || value === SOURCE_LOCALE || !isLanguageTag(value)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** How a page enters translate mode. */
|
|
40
|
+
export interface StartTranslatingOptions {
|
|
41
|
+
/** The locale being translated into, from {@link readTranslateLocale}. */
|
|
42
|
+
locale: string;
|
|
43
|
+
/** Every catalog the page renders messages from. */
|
|
44
|
+
catalogs: readonly CatalogSource[];
|
|
45
|
+
/**
|
|
46
|
+
* The published messages of the locale, by catalog id, so the overlay opens
|
|
47
|
+
* on what the site already ships rather than on English.
|
|
48
|
+
* @default {}
|
|
49
|
+
*/
|
|
50
|
+
translations?: Readonly<Record<string, Messages>>;
|
|
51
|
+
/**
|
|
52
|
+
* The tables whose rows a translator may write.
|
|
53
|
+
* @default []
|
|
54
|
+
*/
|
|
55
|
+
tables?: readonly TranslatableTable[];
|
|
56
|
+
/**
|
|
57
|
+
* Where the overlay is loaded from.
|
|
58
|
+
* @default 'https://translate.cheminfo.org'
|
|
59
|
+
*/
|
|
60
|
+
origin?: string;
|
|
61
|
+
/**
|
|
62
|
+
* The document the overlay script is added to.
|
|
63
|
+
* @default globalThis.document
|
|
64
|
+
*/
|
|
65
|
+
document?: Document;
|
|
66
|
+
/**
|
|
67
|
+
* Told when the overlay cannot be fetched — translate.cheminfo.org down, a
|
|
68
|
+
* network that blocks it, an address typed wrong. The site is expected to
|
|
69
|
+
* stop formatting through the session, so a page nobody can edit goes back
|
|
70
|
+
* to being an ordinary page rather than staying marked with no editor.
|
|
71
|
+
* @default undefined
|
|
72
|
+
*/
|
|
73
|
+
onUnavailable?: () => void;
|
|
74
|
+
/**
|
|
75
|
+
* How long to wait for the overlay before giving up on it, in ms. A server
|
|
76
|
+
* that accepts the connection and then says nothing never fails the script,
|
|
77
|
+
* so waiting for `error` alone would wait for ever.
|
|
78
|
+
* @default 10000
|
|
79
|
+
*/
|
|
80
|
+
timeout?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Put a page into translate mode.
|
|
85
|
+
*
|
|
86
|
+
* The session is exposed before the overlay is loaded, so the overlay finds a
|
|
87
|
+
* page already formatting marked messages. The site is handed the session back
|
|
88
|
+
* and must format through it from then on — that is what makes an edit show up
|
|
89
|
+
* as it is typed.
|
|
90
|
+
* @param options - See {@link StartTranslatingOptions}.
|
|
91
|
+
* @returns The session the page formats through.
|
|
92
|
+
*/
|
|
93
|
+
export function startTranslating(
|
|
94
|
+
options: StartTranslatingOptions,
|
|
95
|
+
): TranslateSession {
|
|
96
|
+
const {
|
|
97
|
+
locale,
|
|
98
|
+
catalogs,
|
|
99
|
+
translations = {},
|
|
100
|
+
tables = [],
|
|
101
|
+
origin = TRANSLATE_ORIGIN,
|
|
102
|
+
document: target = globalThis.document,
|
|
103
|
+
onUnavailable,
|
|
104
|
+
timeout = OVERLAY_TIMEOUT,
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const session = new TranslateSession({
|
|
108
|
+
locale,
|
|
109
|
+
catalogs,
|
|
110
|
+
translations,
|
|
111
|
+
tables,
|
|
112
|
+
marked: true,
|
|
113
|
+
});
|
|
114
|
+
Object.assign(globalThis, { [BRIDGE_GLOBAL]: session });
|
|
115
|
+
loadOverlay(origin, { document: target, onUnavailable, timeout });
|
|
116
|
+
return session;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** How long the overlay has to arrive before the page gives up on it. */
|
|
120
|
+
const OVERLAY_TIMEOUT = 10_000;
|
|
121
|
+
|
|
122
|
+
/** How the overlay script is added, and what happens when it does not come. */
|
|
123
|
+
export interface LoadOverlayOptions {
|
|
124
|
+
/**
|
|
125
|
+
* The document to add it to.
|
|
126
|
+
* @default globalThis.document
|
|
127
|
+
*/
|
|
128
|
+
document?: Document;
|
|
129
|
+
/**
|
|
130
|
+
* Told when it cannot be fetched, or does not arrive in time.
|
|
131
|
+
* @default undefined
|
|
132
|
+
*/
|
|
133
|
+
onUnavailable?: () => void;
|
|
134
|
+
/**
|
|
135
|
+
* How long to wait, in ms.
|
|
136
|
+
* @default 10000
|
|
137
|
+
*/
|
|
138
|
+
timeout?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Add the overlay script to the page, once.
|
|
143
|
+
* @param origin - Where the overlay is served from.
|
|
144
|
+
* @param options - See {@link LoadOverlayOptions}.
|
|
145
|
+
*/
|
|
146
|
+
export function loadOverlay(
|
|
147
|
+
origin: string,
|
|
148
|
+
options: LoadOverlayOptions = {},
|
|
149
|
+
): void {
|
|
150
|
+
const {
|
|
151
|
+
document: target = globalThis.document,
|
|
152
|
+
onUnavailable,
|
|
153
|
+
timeout = OVERLAY_TIMEOUT,
|
|
154
|
+
} = options;
|
|
155
|
+
if (target === undefined) return;
|
|
156
|
+
const src = `${origin.replace(/\/$/, '')}/overlay.js`;
|
|
157
|
+
for (const loaded of target.querySelectorAll('script')) {
|
|
158
|
+
if (loaded.src === src) return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const script = target.createElement('script');
|
|
162
|
+
script.type = 'module';
|
|
163
|
+
script.src = src;
|
|
164
|
+
script.async = true;
|
|
165
|
+
|
|
166
|
+
// The page is only worth marking while there is an editor to use the marks,
|
|
167
|
+
// so a script that never arrives takes translate mode down with it.
|
|
168
|
+
let settled = false;
|
|
169
|
+
const giveUp = () => {
|
|
170
|
+
if (settled) return;
|
|
171
|
+
settled = true;
|
|
172
|
+
script.remove();
|
|
173
|
+
onUnavailable?.();
|
|
174
|
+
};
|
|
175
|
+
const timer = globalThis.setTimeout(giveUp, timeout);
|
|
176
|
+
script.addEventListener('error', giveUp);
|
|
177
|
+
script.addEventListener('load', () => {
|
|
178
|
+
settled = true;
|
|
179
|
+
globalThis.clearTimeout(timer);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
target.head.append(script);
|
|
183
|
+
}
|