不依赖第三方库自己实现 Electron 的主/渲染进程的国际化功能
前言
首先看一下效果(只是为了演示,其中分别为主和渲染进程的文案国际化)。 中文:
英文:
正文
直接贴出代码:
主进程实现
首先需要在 main.ts
文件中获取当前系统的语言,然后初始化国际化相关:
import loadLocale from './locale';
// ......
let locale: I18n.Locale;
// ......
app.on('ready', async () => {
logger.info('app ready');
if (!locale) {
const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
logger.info(`locale: ${appLocale}`);
// 根据系统语言 调用 loadLocale 方法
locale = loadLocale({ appLocale });
}
// ......
createLogin();
});
locale.ts
文件:
import { join } from 'path';
import { readFileSync } from 'fs-extra';
import { app } from 'electron';
import { merge } from 'lodash';
import { setup } from './i18n';
function normalizeLocaleName(locale: string) {
if (/^en-/.test(locale)) {
return 'en';
}
return locale;
}
// 根据系统语言加载对应的 message.json 文件
function getLocaleMessages(locale: string): I18n.Message {
const onDiskLocale = locale.replace('-', '_');
const targetFile = app.isPackaged
? join(process.resourcesPath, '_locales', onDiskLocale, 'messages.json')
: join(__dirname, '../..', '_locales', onDiskLocale, 'messages.json');
return JSON.parse(readFileSync(targetFile, 'utf-8'));
}
export default function loadLocale({
appLocale,
}: { appLocale?: string } = {}): I18n.Locale {
if (!appLocale) {
throw new TypeError('`appLocale` is required');
}
const english = getLocaleMessages('en');
// Load locale - if we can't load messages for the current locale, we
// default to 'en'
//
// possible locales:
// https://github.com/electron/electron/blob/master/docs/api/locales.md
let localeName = normalizeLocaleName(appLocale);
let messages;
try {
messages = getLocaleMessages(localeName);
// We start with english, then overwrite that with anything present in locale
messages = merge(english, messages);
} catch (err) {
console.log(
`Problem loading messages for locale ${localeName} ${err.stack}`
);
console.log('Falling back to en locale');
localeName = 'en';
messages = english;
}
const i18n = setup(appLocale, messages);
return {
i18n,
name: localeName,
messages,
};
}
i18n.ts
文件
const log = typeof window !== 'undefined' ? console : console;
export const setup = (locale: string, messages: I18n.Message) => {
if (!locale) {
throw new Error('i18n: locale parameter is required');
}
if (!messages) {
throw new Error('i18n: messages parameter is required');
}
// 核心方法 用来匹配对应的文案 通过 key, 提供 substitutions 来替换匹配到的占位符
// 其实和渲染进程用到的 getMessage 方案逻辑一致
// getMessage('About ElectronReact'))
// getMessage('EmojiPicker--skin-tone', [`${tone}`])
const getMessage: I18n.I18nFn = (key, substitutions) => {
const entry = messages[key];
if (!entry) {
log.error(
`i18n: Attempted to get translation for nonexistent key '${key}'`
);
return '';
}
if (Array.isArray(substitutions) && substitutions.length > 1) {
throw new Error(
'Array syntax is not supported with more than one placeholder'
);
}
if (
typeof substitutions === 'string' ||
typeof substitutions === 'number'
) {
throw new Error('You must provide either a map or an array');
}
const { message } = entry;
if (!substitutions) {
return message;
}
if (Array.isArray(substitutions)) {
return substitutions.reduce(
(result, substitution) =>
result.toString().replace(/\$.+?\$/, substitution.toString()),
message
);
}
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
let match = FIND_REPLACEMENTS.exec(message);
let builder = '';
let lastTextIndex = 0;
while (match) {
if (lastTextIndex < match.index) {
builder += message.slice(lastTextIndex, match.index);
}
const placeholderName = match[1];
const value = substitutions[placeholderName];
if (!value) {
log.error(
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
);
}
builder += value || '';
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
match = FIND_REPLACEMENTS.exec(message);
}
if (lastTextIndex < message.length) {
builder += message.slice(lastTextIndex);
}
return builder;
};
getMessage.getLocale = () => locale;
return getMessage;
};
然后在主进程中就可以通过 locale.i18n("About ElectronReact")
来实现国际化了。
渲染进程的实现
App 在启动的时候渲染进程已经执行了国际化的初始化,所以在主进程已经保存了一份当前语言的 message.json
信息,所以渲染进程就不需要进程这一步,直接从主进程获取即可。
主进程进程添加一个 locale-data
事件,供渲染进程获取国际化相关数据:
ipcMain.on('locale-data', (event) => {
event.returnValue = locale.messages;
});
首先我们要在 preload 中提前将国际化相关的信息写入到渲染进程的js环境中:
preload.js
:
const localeMessages = ipcRenderer.sendSync('locale-data');
contextBridge.exposeInMainWorld('Context', {
platform: process.platform,
NODE_ENV: process.env.NODE_ENV,
localeMessages,
// ......
});
在渲染进程的代码中(web端项目基于 React,所以使用 Context 来实现):
首先在根组件:
import Login from './login';
import { I18n } from 'Renderer/utils/i18n';
// 获取国际化 json 数据
const { localeMessages } = window.Context;
const Root: React.ComponentType = () => {
return (
<I18n messages={localeMessages}>
<Login />
</I18n>
);
};
export default Root;
utils/i18n.ts
文件:
import { createContext, useCallback, useContext } from 'react';
export type I18nFn = (
key: string,
substitutions?: Array<string | number> | ReplacementValuesType
) => string;
export type ReplacementValuesType = {
[key: string]: string | number;
};
const I18nContext = createContext<I18nFn>(() => 'NO LOCALE LOADED');
export type I18nProps = {
children: React.ReactNode;
messages: { [key: string]: { message: string } };
};
export const I18n: React.ComponentType<I18nProps> = ({
children,
messages,
}): JSX.Element => {
const getMessage = useCallback<I18nFn>(
(key, substitutions) => {
if (Array.isArray(substitutions) && substitutions.length > 1) {
throw new Error(
'Array syntax is not supported with more than one placeholder'
);
}
const { message } = messages[key];
if (!substitutions) {
return message;
}
if (Array.isArray(substitutions)) {
return substitutions.reduce(
(result, substitution) =>
result.toString().replace(/\$.+?\$/, substitution.toString()),
message
) as string;
}
const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
let match = FIND_REPLACEMENTS.exec(message);
let builder = '';
let lastTextIndex = 0;
while (match) {
if (lastTextIndex < match.index) {
builder += message.slice(lastTextIndex, match.index);
}
const placeholderName = match[1];
const value = substitutions[placeholderName];
if (!value) {
// eslint-disable-next-line no-console
console.error(
`i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
);
}
builder += value || '';
lastTextIndex = FIND_REPLACEMENTS.lastIndex;
match = FIND_REPLACEMENTS.exec(message);
}
if (lastTextIndex < message.length) {
builder += message.slice(lastTextIndex);
}
return builder;
},
[messages]
);
return (
<I18nContext.Provider value={getMessage}>{children}</I18nContext.Provider>
);
};
export const useI18n = (): I18nFn => useContext(I18nContext);
然后在实际的组件中可以这么调用:
import { useI18n } from 'Renderer/utils/i18n';
export const DemoComponent:React.FC = () => {
const i18n = useI18n();
return (
<div>
<p>{i18n("About ElectronReact")}</p>
/** 需要替换的值 最终展示为 肤色 tone */
<p>{i18n("EmojiPicker--skin-tone", [`${tone}`])}</p>
</div>
)
}
最后准备好各个语言文案的 json 文件:
zh_CN//message.json
:
{
"About ElectronReact":{
"message": "关于 ElectronReact",
"description": "The text of the login button"
},
"signIn": {
"message": "登录",
"description": "The text of the login button"
},
"signUp": {
"message": "注册",
"description": "The text of the sign up button"
},
"EmojiPicker--empty": {
"message": "没有找到符合条件的表情",
"description": "Shown in the emoji picker when a search yields 0 results."
},
"EmojiPicker--search-placeholder": {
"message": "搜索表情",
"description": "Shown as a placeholder inside the emoji picker search field."
},
"EmojiPicker--skin-tone": {
"message": "肤色 $tone$",
"description": "Shown as a tooltip over the emoji tone buttons.",
"placeholders": {
"status": {
"content": "$1",
"example": "2"
}
}
},
"EmojiPicker__button--recents": {
"message": "最近通话",
"description": "Label for recents emoji picker button"
},
"EmojiPicker__button--emoji": {
"message": "表情符号",
"description": "Label for emoji emoji picker button"
},
"EmojiPicker__button--animal": {
"message": "动物",
"description": "Label for animal emoji picker button"
},
"EmojiPicker__button--food": {
"message": "食物",
"description": "Label for food emoji picker button"
},
"EmojiPicker__button--activity": {
"message": "活动",
"description": "Label for activity emoji picker button"
},
"EmojiPicker__button--travel": {
"message": "旅行",
"description": "Label for travel emoji picker button"
},
"EmojiPicker__button--object": {
"message": "物品",
"description": "Label for object emoji picker button"
},
"EmojiPicker__button--symbol": {
"message": "符号",
"description": "Label for symbol emoji picker button"
},
"EmojiPicker__button--flag": {
"message": "旗帜",
"description": "Label for flag emoji picker button"
},
"sendMessageToContact": {
"message": "发送消息",
"description": "Shown when you are sent a contact and that contact has a signal account"
}
}
en/message.json
:
{
"About ElectronReact":{
"message": "About ElectronReact",
"description": "The text of the login button"
},
"signIn": {
"message": "Sign in",
"description": "The text of the login button"
},
"signUp": {
"message": "Sign up",
"description": "The text of the sign up button"
},
"EmojiPicker--empty": {
"message": "No emoji found",
"description": "Shown in the emoji picker when a search yields 0 results."
},
"EmojiPicker--search-placeholder": {
"message": "Search Emoji",
"description": "Shown as a placeholder inside the emoji picker search field."
},
"EmojiPicker--skin-tone": {
"message": "Skin tone $tone$",
"placeholders": {
"status": {
"content": "$1",
"example": "2"
}
},
"description": "Shown as a tooltip over the emoji tone buttons."
},
"EmojiPicker__button--recents": {
"message": "Recents",
"description": "Label for recents emoji picker button"
},
"EmojiPicker__button--emoji": {
"message": "Emoji",
"description": "Label for emoji emoji picker button"
},
"EmojiPicker__button--animal": {
"message": "Animal",
"description": "Label for animal emoji picker button"
},
"EmojiPicker__button--food": {
"message": "Food",
"description": "Label for food emoji picker button"
},
"EmojiPicker__button--activity": {
"message": "Activity",
"description": "Label for activity emoji picker button"
},
"EmojiPicker__button--travel": {
"message": "Travel",
"description": "Label for travel emoji picker button"
},
"EmojiPicker__button--object": {
"message": "Object",
"description": "Label for object emoji picker button"
},
"EmojiPicker__button--symbol": {
"message": "Symbol",
"description": "Label for symbol emoji picker button"
},
"EmojiPicker__button--flag": {
"message": "Flag",
"description": "Label for flag emoji picker button"
},
"sendMessageToContact": {
"message": "Send Message",
"description": "Shown when you are sent a contact and that contact has a signal account"
}
}
最后
代码都在这里:electron_client
转载自:https://juejin.cn/post/7205462412567986237