feat(parser): comprehensive Markdown parser enhancement - Add nested list support (multi-level unordered/ordered, mixed nesting) - Add Setext headings (=== / ---) - Add indented code blocks (4-space indent) - Add HTML comment passthrough (<!-- -->) - Add entity reference protection (& ©) - Add ordered list start attribute - Add link title single-quote support - Add LaTeX math formulas ($inline$ / $$block$$) - Add footnote support ([^1] ref + [^1]: definition) - Add definition lists (term\n: definition) - Extend emoji map from 85 to 150+ common emojis - Fix backtick code block matching (CommonMark spec) - Fix italic regex character truncation (lookbehind assertions) - Fix superscript/subscript/strikethrough ambiguity - Fix blockquote prefix handling (>text without space) - Enhance safeUrl filtering (additional protocol checks) - Enhance slugify (Unicode NFKC normalization) perf(parser): single-pass inline scanning, render cache, pre-compiled regex - Replace 14-step chained regex with unified single-pass scanInline() - Add cachedRenderInline() with FIFO LRU eviction (300-entry cap) - Pre-compile 15+ static regex constants for block boundary detection - Skip redundant style='text-align:left' on default-aligned table cells style(parser): add CSS for footnotes, definition lists, math formulas, sup/sub test(parser): 44 new test cases covering all v0.1.4 syntax additions (469 total) chore: bump version 0.1.3 → 0.1.4 across all source files, docs, site pages
393 lines
7.9 KiB
JavaScript
393 lines
7.9 KiB
JavaScript
/**
|
|
* MetonaEditor i18n - 国际化管理
|
|
* @module i18n
|
|
* @version 0.1.4
|
|
* @description 多语言支持、语言切换和翻译管理
|
|
*/
|
|
|
|
import { LOCALES } from './constants.js';
|
|
|
|
let currentLocale = 'zh-CN';
|
|
let localeListeners = new Set();
|
|
let fallbackLocale = 'zh-CN';
|
|
|
|
/**
|
|
* 获取当前语言
|
|
*/
|
|
export const getCurrentLocale = () => currentLocale;
|
|
|
|
/**
|
|
* 设置当前语言
|
|
*/
|
|
export const setCurrentLocale = (locale) => {
|
|
let target = locale;
|
|
if (!LOCALES[target]) {
|
|
console.warn(`Locale "${target}" not found, falling back to "${fallbackLocale}"`);
|
|
target = fallbackLocale;
|
|
}
|
|
|
|
currentLocale = target;
|
|
notifyLocaleListeners(target);
|
|
saveLocale(target);
|
|
};
|
|
|
|
/**
|
|
* 翻译函数
|
|
*/
|
|
export const t = (key, params = {}) => {
|
|
const currentTranslation = getTranslation(currentLocale, key);
|
|
if (currentTranslation !== undefined) {
|
|
return interpolate(currentTranslation, params);
|
|
}
|
|
|
|
if (currentLocale !== fallbackLocale) {
|
|
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
|
if (fallbackTranslation !== undefined) {
|
|
return interpolate(fallbackTranslation, params);
|
|
}
|
|
}
|
|
|
|
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
|
return key;
|
|
};
|
|
|
|
/**
|
|
* 获取翻译
|
|
*/
|
|
const getTranslation = (locale, key) => {
|
|
const localeData = LOCALES[locale];
|
|
if (!localeData) return undefined;
|
|
|
|
const keys = key.split('.');
|
|
let result = localeData;
|
|
|
|
for (const k of keys) {
|
|
if (result && typeof result === 'object' && k in result) {
|
|
result = result[k];
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return typeof result === 'string' ? result : undefined;
|
|
};
|
|
|
|
/**
|
|
* 插值函数
|
|
*/
|
|
const interpolate = (str, params) => {
|
|
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
|
return params[key] !== undefined ? params[key] : match;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 检查翻译是否存在
|
|
*/
|
|
export const hasTranslation = (key) => {
|
|
return getTranslation(currentLocale, key) !== undefined ||
|
|
getTranslation(fallbackLocale, key) !== undefined;
|
|
};
|
|
|
|
/**
|
|
* 获取所有翻译
|
|
*/
|
|
export const getTranslations = (locale) => {
|
|
return LOCALES[locale] || {};
|
|
};
|
|
|
|
/**
|
|
* 添加翻译
|
|
*/
|
|
export const addTranslations = (locale, translations) => {
|
|
if (!LOCALES[locale]) {
|
|
LOCALES[locale] = {};
|
|
}
|
|
|
|
deepMerge(LOCALES[locale], translations);
|
|
};
|
|
|
|
/**
|
|
* 深度合并对象
|
|
*/
|
|
const deepMerge = (target, source) => {
|
|
for (const key in source) {
|
|
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
|
deepMerge(target[key], source[key]);
|
|
} else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
return target;
|
|
};
|
|
|
|
/**
|
|
* 获取支持的语言列表
|
|
*/
|
|
export const getSupportedLocales = () => {
|
|
return Object.keys(LOCALES);
|
|
};
|
|
|
|
/**
|
|
* 检查语言是否支持
|
|
*/
|
|
export const isLocaleSupported = (locale) => {
|
|
return locale in LOCALES;
|
|
};
|
|
|
|
/**
|
|
* 获取语言名称
|
|
*/
|
|
export const getLocaleName = (locale) => {
|
|
const names = {
|
|
'zh-CN': '简体中文',
|
|
'zh-TW': '繁體中文',
|
|
'en-US': 'English (US)',
|
|
'en-GB': 'English (UK)',
|
|
'ja': '日本語',
|
|
'ko': '한국어',
|
|
'fr': 'Français',
|
|
'de': 'Deutsch',
|
|
'es': 'Español',
|
|
'pt': 'Português',
|
|
'ru': 'Русский',
|
|
'ar': 'العربية',
|
|
'hi': 'हिन्दी',
|
|
'th': 'ไทย',
|
|
'vi': 'Tiếng Việt',
|
|
'id': 'Bahasa Indonesia',
|
|
'ms': 'Bahasa Melayu',
|
|
'tr': 'Türkçe',
|
|
'it': 'Italiano',
|
|
'nl': 'Nederlands',
|
|
'pl': 'Polski',
|
|
};
|
|
|
|
return names[locale] || locale;
|
|
};
|
|
|
|
/**
|
|
* 获取语言方向
|
|
*/
|
|
export const getLocaleDirection = (locale) => {
|
|
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
|
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
|
};
|
|
|
|
/**
|
|
* 格式化数字
|
|
*/
|
|
export const formatNumber = (number, options = {}) => {
|
|
try {
|
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
|
} catch (e) {
|
|
return number.toString();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 格式化货币
|
|
*/
|
|
export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
|
try {
|
|
return new Intl.NumberFormat(currentLocale, {
|
|
style: 'currency',
|
|
currency,
|
|
...options,
|
|
}).format(amount);
|
|
} catch (e) {
|
|
return amount.toString();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 格式化日期
|
|
*/
|
|
export const formatDate = (date, options = {}) => {
|
|
try {
|
|
const dateObj = date instanceof Date ? date : new Date(date);
|
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
|
} catch (e) {
|
|
return date.toString();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 添加语言监听器
|
|
*/
|
|
export const addLocaleListener = (listener) => {
|
|
localeListeners.add(listener);
|
|
|
|
return () => {
|
|
localeListeners.delete(listener);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 移除语言监听器
|
|
*/
|
|
export const removeLocaleListener = (listener) => {
|
|
localeListeners.delete(listener);
|
|
};
|
|
|
|
/**
|
|
* 通知语言监听器
|
|
*/
|
|
const notifyLocaleListeners = (locale) => {
|
|
localeListeners.forEach((listener) => {
|
|
try {
|
|
listener(locale);
|
|
} catch (e) {
|
|
console.error('Locale listener error:', e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 清除所有语言监听器
|
|
*/
|
|
export const clearLocaleListeners = () => {
|
|
localeListeners.clear();
|
|
};
|
|
|
|
/**
|
|
* 保存语言到本地存储
|
|
*/
|
|
export const saveLocale = (locale) => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
localStorage.setItem('metona-editor-locale', locale);
|
|
} catch (e) {
|
|
console.warn('Failed to save locale:', e);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 从本地存储加载语言
|
|
*/
|
|
export const loadLocale = () => {
|
|
if (typeof localStorage !== 'undefined') {
|
|
try {
|
|
return localStorage.getItem('metona-editor-locale') || getDefaultLocale();
|
|
} catch (e) {
|
|
console.warn('Failed to load locale:', e);
|
|
}
|
|
}
|
|
return getDefaultLocale();
|
|
};
|
|
|
|
/**
|
|
* 获取默认语言
|
|
*/
|
|
export const getDefaultLocale = () => {
|
|
if (typeof navigator !== 'undefined') {
|
|
const browserLocale = navigator.language || navigator.userLanguage;
|
|
if (browserLocale && isLocaleSupported(browserLocale)) {
|
|
return browserLocale;
|
|
}
|
|
|
|
const shortLocale = browserLocale?.split('-')[0];
|
|
if (shortLocale && isLocaleSupported(shortLocale)) {
|
|
return shortLocale;
|
|
}
|
|
}
|
|
|
|
return fallbackLocale;
|
|
};
|
|
|
|
/**
|
|
* 初始化国际化系统
|
|
*/
|
|
export const initI18n = () => {
|
|
const savedLocale = loadLocale();
|
|
setCurrentLocale(savedLocale);
|
|
};
|
|
|
|
/**
|
|
* 切换语言
|
|
*/
|
|
export const switchLocale = (locale) => {
|
|
setCurrentLocale(locale);
|
|
};
|
|
|
|
/**
|
|
* 国际化工具 — 代理层
|
|
*/
|
|
export const i18nUtils = {
|
|
t,
|
|
getCurrentLocale,
|
|
setCurrentLocale,
|
|
switchLocale,
|
|
getFallbackLocale: () => fallbackLocale,
|
|
setFallbackLocale: (locale) => { fallbackLocale = locale; },
|
|
hasTranslation,
|
|
getTranslations,
|
|
addTranslations,
|
|
getSupportedLocales,
|
|
isLocaleSupported,
|
|
getLocaleName,
|
|
getLocaleDirection,
|
|
formatNumber,
|
|
formatCurrency,
|
|
formatDate,
|
|
addLocaleListener,
|
|
removeLocaleListener,
|
|
clearLocaleListeners,
|
|
initI18n,
|
|
saveLocale,
|
|
loadLocale,
|
|
getDefaultLocale,
|
|
};
|
|
|
|
/**
|
|
* 预设语言包
|
|
*/
|
|
export const presetLocales = {
|
|
'zh-CN': {
|
|
name: '简体中文',
|
|
nativeName: '简体中文',
|
|
direction: 'ltr',
|
|
translations: LOCALES['zh-CN'],
|
|
},
|
|
'en-US': {
|
|
name: 'English (US)',
|
|
nativeName: 'English (US)',
|
|
direction: 'ltr',
|
|
translations: LOCALES['en-US'],
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 创建国际化管理器
|
|
*/
|
|
export const createI18nManager = () => {
|
|
return {
|
|
t,
|
|
getCurrentLocale,
|
|
setCurrentLocale,
|
|
switchLocale,
|
|
getFallbackLocale: () => fallbackLocale,
|
|
setFallbackLocale: (locale) => { fallbackLocale = locale; },
|
|
hasTranslation,
|
|
getTranslations,
|
|
addTranslations,
|
|
getSupportedLocales,
|
|
isLocaleSupported,
|
|
getLocaleName,
|
|
getLocaleDirection,
|
|
formatNumber,
|
|
formatCurrency,
|
|
formatDate,
|
|
addLocaleListener,
|
|
removeLocaleListener,
|
|
clearLocaleListeners,
|
|
initI18n,
|
|
saveLocale,
|
|
loadLocale,
|
|
getDefaultLocale,
|
|
};
|
|
};
|
|
|
|
export { i18nUtils as default };
|