wayBetter for X 1.0.2
This commit is contained in:
parent
7e2515aa42
commit
467aea2e28
212 changed files with 10208 additions and 2345 deletions
107
apps/desktop/src/inject/xRichText.ts
Normal file
107
apps/desktop/src/inject/xRichText.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// X's rich-text sentences (a link or bold inside a sentence) for string packs. X renders them with a
|
||||
// class registered in each language chunk as `_ActualI18NFormatMessage`: its getters (one per key
|
||||
// id) return the text pieces, and `render` shows piece n followed by the caller's element n through
|
||||
// `templateReducer`; the callers use `I18NFormatMessage`, an arrow function that creates an element
|
||||
// of that class. A pack never touches the class or the getters. This adapter registers a subclass in
|
||||
// its place whose `render` asks the engine for the key's pieces: a pack's replacement (pieces and
|
||||
// the order of X's own elements, `RichTemplate`) is shown with the caller's real elements, a
|
||||
// rewritten array goes through X's own reducer, anything else is X's own render. Every step fails
|
||||
// open: an exception shows X's own sentence.
|
||||
import type { RichTemplate } from '../../../../waybetter/packages/runtime/src/stringPacks.ts';
|
||||
|
||||
export const RICH_CLASS = '_ActualI18NFormatMessage';
|
||||
export const RICH_ELEMENT = 'I18NFormatMessage';
|
||||
|
||||
/** The parts of React the renderer needs; found among what the language chunk itself requires. */
|
||||
export type ReactLike = {
|
||||
createElement: (...args: unknown[]) => unknown;
|
||||
Fragment: unknown;
|
||||
Children: { toArray(children: unknown): unknown[] };
|
||||
};
|
||||
export const isReact = (value: unknown): value is ReactLike => {
|
||||
const react = value as Partial<ReactLike> | null;
|
||||
return typeof react === 'object' && react !== null && typeof react.createElement === 'function' && react.Fragment !== undefined && typeof react.Children?.toArray === 'function';
|
||||
};
|
||||
|
||||
type Props = { $i18n?: unknown; children?: unknown };
|
||||
type Instance = { props: Props; templateReducer: (...args: never[]) => unknown } & Record<string, unknown>;
|
||||
|
||||
const isTemplate = (value: unknown): value is RichTemplate =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value) && Array.isArray((value as RichTemplate).pieces) && Array.isArray((value as RichTemplate).order);
|
||||
|
||||
/** The renderer for one language chunk. `resolve(id, pieces)` is the engine's answer for a key. */
|
||||
export function richText(resolve: (id: string, original: unknown) => unknown) {
|
||||
let react: ReactLike | null = null;
|
||||
let original: (new (...args: never[]) => unknown) | null = null;
|
||||
let replacement: unknown = null;
|
||||
|
||||
/** `_ActualI18NFormatMessage`: X's class, or a subclass that asks the engine. */
|
||||
function theClass(value: unknown): unknown {
|
||||
if (typeof value !== 'function' || !react) return value;
|
||||
try {
|
||||
const Base = value as new (...args: never[]) => { render(): unknown };
|
||||
const React = react;
|
||||
class Replaced extends Base {
|
||||
render(): unknown {
|
||||
try {
|
||||
const self = this as unknown as Instance;
|
||||
const id = self.props.$i18n;
|
||||
if (typeof id !== 'string') return super.render();
|
||||
const pieces = self[id];
|
||||
const result = resolve(id, pieces);
|
||||
if (isTemplate(result)) {
|
||||
const elements = React.Children.toArray(self.props.children);
|
||||
const out: unknown[] = [React.Fragment, null];
|
||||
result.pieces.forEach((piece, index) => {
|
||||
out.push(piece);
|
||||
if (index < result.order.length) out.push(elements[result.order[index]!]);
|
||||
});
|
||||
return React.createElement(...out);
|
||||
}
|
||||
if (Array.isArray(result) && result !== pieces) {
|
||||
return React.createElement(...(result.reduce(self.templateReducer as never, [React.Fragment, null]) as unknown[]));
|
||||
}
|
||||
} catch {
|
||||
// X's own sentence below.
|
||||
}
|
||||
return super.render();
|
||||
}
|
||||
}
|
||||
original = Base;
|
||||
replacement = Replaced;
|
||||
return Replaced;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/** `I18NFormatMessage`: X's arrow, with the element of X's class made of the replacement instead. */
|
||||
function theElement(value: unknown): unknown {
|
||||
if (typeof value !== 'function' || !replacement || !react) return value;
|
||||
const make = value as (props: unknown) => { type?: unknown; props?: unknown; key?: unknown } | null;
|
||||
const React = react;
|
||||
return function (this: unknown, props: unknown) {
|
||||
const element = make.call(this, props);
|
||||
try {
|
||||
if (element && element.type === original) {
|
||||
const own = (element.props ?? {}) as Record<string, unknown>;
|
||||
return React.createElement(replacement, element.key == null ? own : { ...own, key: element.key });
|
||||
}
|
||||
} catch {
|
||||
// X's own element below.
|
||||
}
|
||||
return element;
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
/** Offered every module a language chunk requires: React is remembered, nothing is replaced. */
|
||||
seen(exports: unknown): void { if (!react && isReact(exports)) react = exports; },
|
||||
/** What to register for a key: the replacement for X's two rich-text entries, `undefined` for any other key. */
|
||||
intercept(id: string, value: unknown): { value: unknown } | undefined {
|
||||
if (id === RICH_CLASS) return { value: theClass(value) };
|
||||
if (id === RICH_ELEMENT) return { value: theElement(value) };
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue