Commit f9b32df6 authored by nanahira's avatar nanahira

add patchStringInObject

parent 682a4736
...@@ -6,3 +6,4 @@ export * from './src/abortable'; ...@@ -6,3 +6,4 @@ export * from './src/abortable';
export * from './src/types'; export * from './src/types';
export * from './src/middleware-dispatcher'; export * from './src/middleware-dispatcher';
export * from './src/i18n'; export * from './src/i18n';
export * from './src/patch-string-in-object';
import { MiddlewareDispatcher } from '../middleware-dispatcher'; import { MiddlewareDispatcher } from '../middleware-dispatcher';
import { parseI18n } from '../utility/parse-i18n'; import { parseI18n } from '../utility/parse-i18n';
import { I18nMiddleware, I18nOptions } from './types'; import { I18nMiddleware, I18nOptions } from './types';
import { patchStringInObject } from '../patch-string-in-object';
export class I18n<Ex extends any[] = []> { export class I18n<Ex extends any[] = []> {
constructor(private options: I18nOptions) {} constructor(private options: I18nOptions) {}
...@@ -143,156 +144,8 @@ export class I18n<Ex extends any[] = []> { ...@@ -143,156 +144,8 @@ export class I18n<Ex extends any[] = []> {
} }
async translate<T>(locale: string, obj: T, ...ex: Ex): Promise<T> { async translate<T>(locale: string, obj: T, ...ex: Ex): Promise<T> {
const visited = new WeakSet<object>(); return patchStringInObject(obj, (s) =>
this.translateString(locale, s, ...ex),
const isBuiltInObject = (v: any): boolean => { );
if (v == null || typeof v !== 'object') return false;
if (
v instanceof Date ||
v instanceof RegExp ||
v instanceof Map ||
v instanceof Set ||
v instanceof WeakMap ||
v instanceof WeakSet ||
v instanceof ArrayBuffer ||
v instanceof DataView ||
ArrayBuffer.isView(v) // TypedArray / Buffer
)
return true;
const tag = Object.prototype.toString.call(v);
switch (tag) {
case '[object URL]':
case '[object URLSearchParams]':
case '[object Error]':
case '[object Blob]':
case '[object File]':
case '[object FormData]':
return true;
default:
return false;
}
};
const translateObjectPreservingProto = async (value: any): Promise<any> => {
const proto = Object.getPrototypeOf(value);
const out = Object.create(proto);
const keys = Reflect.ownKeys(value);
await Promise.all(
keys.map(async (key) => {
const desc = Object.getOwnPropertyDescriptor(value, key as any);
if (!desc) return;
// 数据属性:并发递归其 value,保持原属性特性
if ('value' in desc) {
const newVal = await visit(desc.value);
Object.defineProperty(out, key, { ...desc, value: newVal });
return;
}
// 访问器属性:先复制 getter/setter,再尝试读值→翻译→写回
Object.defineProperty(out, key, desc);
// 没有 getter 就无需处理;有 getter 但抛错也忽略
let current: any = undefined;
if (typeof desc.get === 'function') {
try {
current = desc.get.call(value);
} catch {
/* ignore */
}
}
if (current === undefined) return;
// 递归翻译 getter 返回的值;若有 setter 则写回
try {
const newVal = await visit(current);
if (typeof desc.set === 'function') {
try {
desc.set.call(out, newVal);
} catch {
/* ignore */
}
}
} catch {
// 翻译失败不影响其他键
}
}),
);
return out;
};
const isTranslatable = (
v: any,
): { ok: false } | { ok: true; kind: 'string' | 'object' } => {
// 1) 所有 falsy 原样(null/undefined/false/0/''/NaN)
if (!v) return { ok: false };
// 2) 基本类型过滤
const t = typeof v;
if (
t === 'number' ||
t === 'bigint' ||
t === 'symbol' ||
t === 'function'
) {
return { ok: false };
}
// 3) 可翻译:字符串 or 对象(对象再由后续逻辑决定是否深入)
if (t === 'string') return { ok: true, kind: 'string' };
if (t === 'object') {
return { ok: true, kind: 'object' };
}
// 其它(boolean 等已包含在 falsy/基本类型里)
return { ok: false };
};
const visit = async (value: any): Promise<any> => {
// 1) 检查是否可翻译
const check = isTranslatable(value);
// 2) 不可翻译的原样返回
if (!check.ok) {
return value;
}
if (check.kind === 'string') {
// 3) 字符串:翻译
return this.translateString(locale, value, ...ex);
}
// 4) Promise:不隐式展开(保持语义)
if (value instanceof Promise) {
return value.then((resolved) => visit(resolved));
}
// 5) 对象类:过滤内置对象
if (typeof value === 'object') {
if (!Array.isArray(value) && isBuiltInObject(value)) return value;
// 防环
if (visited.has(value)) return value;
visited.add(value);
// 数组:元素级递归
if (Array.isArray(value)) {
const out = await Promise.all(value.map((v) => visit(v)));
return out as any;
}
// 其他对象(含类实例/DTO):保留原型并递归自有属性
return translateObjectPreservingProto(value);
}
// 其余类型(boolean 等)原样返回
return value;
};
return visit(obj);
} }
} }
import { Awaitable } from './types';
export const patchStringInObject = async <T>(
obj: T,
cb: (s: string) => Awaitable<string>,
): Promise<T> => {
const visited = new WeakSet<object>();
const isSpecialObject = (v: any): boolean => {
if (v == null || typeof v !== 'object') return false;
if (
v instanceof Date ||
v instanceof RegExp ||
v instanceof Map ||
v instanceof Set ||
v instanceof WeakMap ||
v instanceof WeakSet ||
v instanceof ArrayBuffer ||
v instanceof DataView ||
ArrayBuffer.isView(v)
) {
return true;
}
const tag = Object.prototype.toString.call(v);
switch (tag) {
case '[object URL]':
case '[object URLSearchParams]':
case '[object Error]':
case '[object Blob]':
case '[object File]':
case '[object FormData]':
return true;
default:
return false;
}
};
const checkValue = (
v: any,
): { ok: false } | { ok: true; kind: 'string' | 'object' } => {
if (!v) return { ok: false };
const t = typeof v;
if (
t === 'number' ||
t === 'bigint' ||
t === 'symbol' ||
t === 'function'
) {
return { ok: false };
}
if (t === 'string') return { ok: true, kind: 'string' };
if (t === 'object') return { ok: true, kind: 'object' };
return { ok: false };
};
const rebuildObject = async (value: any): Promise<any> => {
const proto = Object.getPrototypeOf(value);
const out = Object.create(proto);
const keys = Reflect.ownKeys(value);
await Promise.all(
keys.map(async (key) => {
const desc = Object.getOwnPropertyDescriptor(value, key as any);
if (!desc) return;
if ('value' in desc) {
const newVal = await walk(desc.value);
Object.defineProperty(out, key, { ...desc, value: newVal });
return;
}
Object.defineProperty(out, key, desc);
let current: any = undefined;
if (typeof desc.get === 'function') {
try {
current = desc.get.call(value);
} catch {}
}
if (current === undefined) return;
try {
const newVal = await walk(current);
if (typeof desc.set === 'function') {
try {
desc.set.call(out, newVal);
} catch {}
}
} catch {}
}),
);
return out;
};
const walk = async (value: any): Promise<any> => {
const check = checkValue(value);
if (!check.ok) return value;
if (check.kind === 'string') {
return cb(value);
}
if (value instanceof Promise) {
return value.then((resolved) => walk(resolved));
}
if (typeof value === 'object') {
if (!Array.isArray(value) && isSpecialObject(value)) return value;
if (visited.has(value)) return value;
visited.add(value);
if (Array.isArray(value)) {
const out = await Promise.all(value.map((v) => walk(v)));
return out as any;
}
return rebuildObject(value);
}
return value;
};
return walk(obj);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment