正文
直接贴出代码:
主进程实现
首先需要在 main.ts
文件中获取当前系统的语言,然后初始化国际化相关:
1import loadLocale from './locale';
2
3
4let locale: I18n.Locale;
5
6
7app.on('ready', async () => {
8 logger.info('app ready');
9
10 if (!locale) {
11 const appLocale = process.env.NODE_ENV === 'test' ? 'en' : app.getLocale();
12 logger.info(`locale: ${appLocale}`);
13
14 locale = loadLocale({ appLocale });
15 }
16
17
18 createLogin();
19});
locale.ts
文件:
1import { join } from 'path';
2import { readFileSync } from 'fs-extra';
3import { app } from 'electron';
4import { merge } from 'lodash';
5import { setup } from './i18n';
6
7function normalizeLocaleName(locale: string) {
8 if (/^en-/.test(locale)) {
9 return 'en';
10 }
11
12 return locale;
13}
14
15
16function getLocaleMessages(locale: string): I18n.Message {
17 const onDiskLocale = locale.replace('-', '_');
18
19 const targetFile = app.isPackaged
20 ? join(process.resourcesPath, '_locales', onDiskLocale, 'messages.json')
21 : join(__dirname, '../..', '_locales', onDiskLocale, 'messages.json');
22
23 return JSON.parse(readFileSync(targetFile, 'utf-8'));
24}
25
26export default function loadLocale({
27 appLocale,
28}: { appLocale?: string } = {}): I18n.Locale {
29 if (!appLocale) {
30 throw new TypeError('`appLocale` is required');
31 }
32
33 const english = getLocaleMessages('en');
34
35
36 let localeName = normalizeLocaleName(appLocale);
37 let messages;
38
39 try {
40 messages = getLocaleMessages(localeName);
41
42
43 messages = merge(english, messages);
44 } catch (err) {
45 console.log(
46 `Problem loading messages for locale ${localeName} ${err.stack}`
47 );
48 console.log('Falling back to en locale');
49
50 localeName = 'en';
51 messages = english;
52 }
53
54 const i18n = setup(appLocale, messages);
55
56 return {
57 i18n,
58 name: localeName,
59 messages,
60 };
61}
i18n.ts
文件
1const log = typeof window !== 'undefined' ? console : console;
2
3export const setup = (locale: string, messages: I18n.Message) => {
4 if (!locale) {
5 throw new Error('i18n: locale parameter is required');
6 }
7 if (!messages) {
8 throw new Error('i18n: messages parameter is required');
9 }
10
11
12 const getMessage: I18n.I18nFn = (key, substitutions) => {
13 const entry = messages[key];
14 if (!entry) {
15 log.error(
16 `i18n: Attempted to get translation for nonexistent key '${key}'`
17 );
18 return '';
19 }
20 if (Array.isArray(substitutions) && substitutions.length > 1) {
21 throw new Error(
22 'Array syntax is not supported with more than one placeholder'
23 );
24 }
25 if (
26 typeof substitutions === 'string' ||
27 typeof substitutions === 'number'
28 ) {
29 throw new Error('You must provide either a map or an array');
30 }
31
32 const { message } = entry;
33 if (!substitutions) {
34 return message;
35 }
36 if (Array.isArray(substitutions)) {
37 return substitutions.reduce(
38 (result, substitution) =>
39 result.toString().replace(/\$.+?\$/, substitution.toString()),
40 message
41 );
42 }
43
44 const FIND_REPLACEMENTS = /\$([^$]+)\$/g;
45
46 let match = FIND_REPLACEMENTS.exec(message);
47 let builder = '';
48 let lastTextIndex = 0;
49
50 while (match) {
51 if (lastTextIndex < match.index) {
52 builder += message.slice(lastTextIndex, match.index);
53 }
54
55 const placeholderName = match[1];
56 const value = substitutions[placeholderName];
57 if (!value) {
58 log.error(
59 `i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
60 );
61 }
62 builder += value || '';
63
64 lastTextIndex = FIND_REPLACEMENTS.lastIndex;
65 match = FIND_REPLACEMENTS.exec(message);
66 }
67
68 if (lastTextIndex < message.length) {
69 builder += message.slice(lastTextIndex);
70 }
71
72 return builder;
73 };
74
75 getMessage.getLocale = () => locale;
76
77 return getMessage;
78};
79
然后在主进程中就可以通过 locale.i18n("About ElectronReact")
来实现国际化了。
渲染进程的实现
App 在启动的时候渲染进程已经执行了国际化的初始化,所以在主进程已经保存了一份当前语言的 message.json
信息,所以渲染进程就不需要进程这一步,直接从主进程获取即可。
主进程进程添加一个 locale-data
事件,供渲染进程获取国际化相关数据:
1ipcMain.on('locale-data', (event) => {
2 event.returnValue = locale.messages;
3});
首先我们要在 preload 中提前将国际化相关的信息写入到渲染进程的js环境中:
preload.js
:
1const localeMessages = ipcRenderer.sendSync('locale-data');
2
3contextBridge.exposeInMainWorld('Context', {
4 platform: process.platform,
5 NODE_ENV: process.env.NODE_ENV,
6
7 localeMessages,
8
9
10 });
11
在渲染进程的代码中(web端项目基于 React,所以使用 Context 来实现):
首先在根组件:
1import Login from './login';
2import { I18n } from 'Renderer/utils/i18n';
3
4
5const { localeMessages } = window.Context;
6
7const Root: React.ComponentType = () => {
8 return (
9 <I18n messages={localeMessages}>
10 <Login />
11 </I18n>
12 );
13};
14
15export default Root;
16
utils/i18n.ts
文件:
1import { createContext, useCallback, useContext } from 'react'
2
3export type I18nFn = (
4 key: string,
5 substitutions?: Array<string | number> | ReplacementValuesType
6) => string
7
8export type ReplacementValuesType = {
9 [key: string]: string | number
10}
11
12const I18nContext = createContext<I18nFn>(() => 'NO LOCALE LOADED')
13
14export type I18nProps = {
15 children: React.ReactNode
16 messages: { [key: string]: { message: string } }
17}
18
19export const I18n: React.ComponentType<I18nProps> = ({
20 children,
21 messages,
22}): JSX.Element => {
23 const getMessage = useCallback<I18nFn>(
24 (key, substitutions) => {
25 if (Array.isArray(substitutions) && substitutions.length > 1) {
26 throw new Error(
27 'Array syntax is not supported with more than one placeholder'
28 )
29 }
30
31 const { message } = messages[key]
32 if (!substitutions) {
33 return message
34 }
35
36 if (Array.isArray(substitutions)) {
37 return substitutions.reduce(
38 (result, substitution) =>
39 result.toString().replace(/\$.+?\$/, substitution.toString()),
40 message
41 ) as string
42 }
43
44 const FIND_REPLACEMENTS = /\$([^$]+)\$/g
45
46 let match = FIND_REPLACEMENTS.exec(message)
47 let builder = ''
48 let lastTextIndex = 0
49
50 while (match) {
51 if (lastTextIndex < match.index) {
52 builder += message.slice(lastTextIndex, match.index)
53 }
54
55 const placeholderName = match[1]
56 const value = substitutions[placeholderName]
57 if (!value) {
58 // eslint-disable-next-line no-console
59 console.error(
60 `i18n: Value not provided for placeholder ${placeholderName} in key '${key}'`
61 )
62 }
63 builder += value || ''
64
65 lastTextIndex = FIND_REPLACEMENTS.lastIndex
66 match = FIND_REPLACEMENTS.exec(message)
67 }
68
69 if (lastTextIndex < message.length) {
70 builder += message.slice(lastTextIndex)
71 }
72
73 return builder
74 },
75 [messages]
76 )
77
78 return (
79 <I18nContext.Provider value={getMessage}>{children}</I18nContext.Provider>
80 )
81}
82
83export const useI18n = (): I18nFn => useContext(I18nContext)
84
然后在实际的组件中可以这么调用:
1import { useI18n } from 'Renderer/utils/i18n';
2
3export const DemoComponent:React.FC = () => {
4 const i18n = useI18n();
5
6 return (
7 <div>
8 <p>{i18n("About ElectronReact")}</p>
9 /** 需要替换的值 最终展示为 肤色 tone */
10 <p>{i18n("EmojiPicker--skin-tone", [`${tone}`])}</p>
11 </div>
12 )
13}
最后准备好各个语言文案的 json 文件:
zh_CN//message.json
:
1{
2 "About ElectronReact":{
3 "message": "关于 ElectronReact",
4 "description": "The text of the login button"
5 },
6 "signIn": {
7 "message": "登录",
8 "description": "The text of the login button"
9 },
10 "signUp": {
11 "message": "注册",
12 "description": "The text of the sign up button"
13 },
14 "EmojiPicker--empty": {
15 "message": "没有找到符合条件的表情",
16 "description": "Shown in the emoji picker when a search yields 0 results."
17 },
18 "EmojiPicker--search-placeholder": {
19 "message": "搜索表情",
20 "description": "Shown as a placeholder inside the emoji picker search field."
21 },
22 "EmojiPicker--skin-tone": {
23 "message": "肤色 $tone$",
24 "description": "Shown as a tooltip over the emoji tone buttons.",
25 "placeholders": {
26 "status": {
27 "content": "$1",
28 "example": "2"
29 }
30 }
31 },
32 "EmojiPicker__button--recents": {
33 "message": "最近通话",
34 "description": "Label for recents emoji picker button"
35 },
36 "EmojiPicker__button--emoji": {
37 "message": "表情符号",
38 "description": "Label for emoji emoji picker button"
39 },
40 "EmojiPicker__button--animal": {
41 "message": "动物",
42 "description": "Label for animal emoji picker button"
43 },
44 "EmojiPicker__button--food": {
45 "message": "食物",
46 "description": "Label for food emoji picker button"
47 },
48 "EmojiPicker__button--activity": {
49 "message": "活动",
50 "description": "Label for activity emoji picker button"
51 },
52 "EmojiPicker__button--travel": {
53 "message": "旅行",
54 "description": "Label for travel emoji picker button"
55 },
56 "EmojiPicker__button--object": {
57 "message": "物品",
58 "description": "Label for object emoji picker button"
59 },
60 "EmojiPicker__button--symbol": {
61 "message": "符号",
62 "description": "Label for symbol emoji picker button"
63 },
64 "EmojiPicker__button--flag": {
65 "message": "旗帜",
66 "description": "Label for flag emoji picker button"
67 },
68 "sendMessageToContact": {
69 "message": "发送消息",
70 "description": "Shown when you are sent a contact and that contact has a signal account"
71 }
72}
en/message.json
:
1{
2 "About ElectronReact":{
3 "message": "About ElectronReact",
4 "description": "The text of the login button"
5 },
6 "signIn": {
7 "message": "Sign in",
8 "description": "The text of the login button"
9 },
10 "signUp": {
11 "message": "Sign up",
12 "description": "The text of the sign up button"
13 },
14 "EmojiPicker--empty": {
15 "message": "No emoji found",
16 "description": "Shown in the emoji picker when a search yields 0 results."
17 },
18 "EmojiPicker--search-placeholder": {
19 "message": "Search Emoji",
20 "description": "Shown as a placeholder inside the emoji picker search field."
21 },
22 "EmojiPicker--skin-tone": {
23 "message": "Skin tone $tone$",
24 "placeholders": {
25 "status": {
26 "content": "$1",
27 "example": "2"
28 }
29 },
30 "description": "Shown as a tooltip over the emoji tone buttons."
31 },
32 "EmojiPicker__button--recents": {
33 "message": "Recents",
34 "description": "Label for recents emoji picker button"
35 },
36 "EmojiPicker__button--emoji": {
37 "message": "Emoji",
38 "description": "Label for emoji emoji picker button"
39 },
40 "EmojiPicker__button--animal": {
41 "message": "Animal",
42 "description": "Label for animal emoji picker button"
43 },
44 "EmojiPicker__button--food": {
45 "message": "Food",
46 "description": "Label for food emoji picker button"
47 },
48 "EmojiPicker__button--activity": {
49 "message": "Activity",
50 "description": "Label for activity emoji picker button"
51 },
52 "EmojiPicker__button--travel": {
53 "message": "Travel",
54 "description": "Label for travel emoji picker button"
55 },
56 "EmojiPicker__button--object": {
57 "message": "Object",
58 "description": "Label for object emoji picker button"
59 },
60 "EmojiPicker__button--symbol": {
61 "message": "Symbol",
62 "description": "Label for symbol emoji picker button"
63 },
64 "EmojiPicker__button--flag": {
65 "message": "Flag",
66 "description": "Label for flag emoji picker button"
67 },
68 "sendMessageToContact": {
69 "message": "Send Message",
70 "description": "Shown when you are sent a contact and that contact has a signal account"
71 }
72}
最后
代码都在这里:electron_client
个人笔记记录 2021 ~ 2025