不依赖第三方库自己实现 Electron 的主与渲染进程的国际化功能
2025-03-05
344 字约 2 分钟
...正文
直接贴出代码:
主进程实现
首先需要在 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}`);
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;
}
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');
let localeName = normalizeLocaleName(appLocale);
let messages;
try {
messages = getLocaleMessages(localeName);
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');
}
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';
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
如果您觉得这篇文章有帮助,请点个赞吧~
相关文章
更多文章 →electron2025-03-05
应用多开,限制只启动一个应用,防止多个实例
通过app.requestSingleInstanceLock来控制应用的多开,返回值为boolean。 此方法的返回值表示你的应用程序实例是否成功取得了锁。如果它取得锁失败,你可以假设另一个应用实例已经取得了锁并且仍旧在运行,并立即退出。 即:如果当前进程是应用程序的主要实例,则此方法返回true,同时你的应用会继续运行。如果当它返回false,如果你的程序没有取得锁,它应该立刻退出,并且将参数发送给那个已经取到锁的进程。 在macO...
学习
electron2024-09-23
Electron实现文件缓存背景
背景 基于Electron研发的一款IM企业通信桌面端应用,会存在非常多文件、图片、视频类型回话消息。在Electron应用(桌面客户端软件)中,快速加载并显示图片是提升用户体验的关键。然而,传统的图片加载方式往往存在加载速度慢、资源占用高等问题,影响了用户的使用体验。 解决的问题 1. 支持自定义配置存储的磁盘位置 2. 支持长期存储 3. 支持自定义存储大小 4. 支持自定义存储类型(如图片、视频、文件,或者更细致化到MIME) 5...
学习
electron2024-09-23
我的 Electron 客户端也可以全量和增量更新了
前言 本文主要介绍 客户端应用的自动更新,包括全量和增量这两种方式。 全量更新: 运行新的安装包,安装目录所有资源覆盖式更新。 增量更新: 只更新修改的部分,通常是渲染进程和主进程文件。 本文并没有拿真实项目来举例子,而是起一个 新的项目 从 0 到 1 来实现自动更新。 如果已有的项目需要支持该功能,借鉴本文的主要步骤即可。 前置说明: 1. 由于业务场景的限制,本文介绍的更新仅支持 操作系统,其余操作系统未作兼容处理。 2. 更新流...
学习
electron2024-09-20
我的 Electron 客户端被第三方页面入侵了
问题描述 公司有个内部项目是用 来开发的,有个功能需要像浏览器一样加载第三方站点。 本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面 。 这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码。 翻译一下就是:如果当前窗口不是顶级窗口的话,将当前窗口设置为顶级窗口。 奇怪的是两者不是 跨域 了吗,为什么 还可以影响顶级窗口。 先说一下我当时的一些解决办法: 1. 用 替换 2. 给 添...
学习
electron2024-09-13
Electron+WebRTC实现局域网内的远程控制软件
我的工位上有一台笔记本(windows),一个显示器,一台服务器(linux)。显示器同时连接笔记本和服务器,当需要用到服务器的时候:1.使用todesk等软件进行连接;2.直接把显示器切换到服务器桌面,然后给服务器插上鼠标键盘。在这种场景下,屏幕画面的传输对我来说并不是必要的,这种场景下能不能用一套键鼠控制两个电脑呢?边做边学便有了这个小工具。记录下来,其中遇到的问题和解决方案。 通过electron + Vue + primevue...
学习
electron2024-09-11
如何将 Electron 项目上架
前言 是一个开源框架,它允许开发者使用 技术( 、 和 )来构建跨平台的桌面应用程序。 应用程序可以运行在 、 和 上,为用户提供了一种统一的方式来开发和维护软件。 本文将探讨如何将 Electron 构建的桌面应用程序上架到 中。 创建证书 进入苹果开发者平台 进入证书列表: 创建证书(Certificates) 用于对需要上架应用进行签名 创建 和 两个证书。 证书用于在 网站上注册的计算机上签署用于开发和测试的应用程序。 注册方法...
学习
评论
请登录后发表评论
去登录