Skip to content

Commit

Permalink
feat: enable i18n keys in plugin infra
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed Jul 13, 2021
1 parent 16b6a31 commit a5930fd
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 28 deletions.
36 changes: 20 additions & 16 deletions packages/maskbook/src/components/InjectedComponents/PostDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
DialogContent,
DialogActions,
} from '@material-ui/core'
import { Plugin, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra'
import { I18NStringField, Plugin, useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra'
import { useValueRef } from '@masknet/shared'
import { CompositionEvent, MaskMessage, useI18N, Flags } from '../../utils'
import { isMinds } from '../../social-network-adaptor/minds.com/base'
Expand All @@ -41,6 +41,7 @@ import { DebugMetadataInspector } from '../shared/DebugMetadataInspector'
import { editActivatedPostMetadata, globalTypedMessageMetadata } from '../../protocols/typed-message/global-state'
import { isTwitter } from '../../social-network-adaptor/twitter.com/base'
import { SteganographyTextPayload } from './SteganographyTextPayload'
import { PluginI18NFieldRender, usePluginI18NField } from '../../plugin-infra/I18NFieldRender'

const useStyles = makeStyles({
MUIInputRoot: {
Expand Down Expand Up @@ -457,6 +458,7 @@ export function CharLimitIndicator({ value, max, ...props }: CircularProgressPro

function PluginRenderer() {
const chainIdValid = useChainIdValid()
const pluginField = usePluginI18NField()
const result = useActivatedPluginsSNSAdaptor().map((plugin) =>
Result.wrap(() => {
const entry = plugin.CompositionDialogEntry
Expand All @@ -465,7 +467,7 @@ function PluginRenderer() {
if (!entry || (!chainIdValid && requireChainValid)) return null

return (
<ErrorBoundary subject={`Plugin "${plugin.name.fallback}"`} key={plugin.ID}>
<ErrorBoundary subject={`Plugin "${pluginField(plugin.ID, plugin.name)}"`} key={plugin.ID}>
{'onClick' in entry ? (
<PluginKindCustom {...entry} unstable={unstable} id={plugin.ID} />
) : (
Expand All @@ -479,6 +481,7 @@ function PluginRenderer() {
}
function BadgeRenderer({ meta }: { meta: TypedMessage['meta'] }) {
const plugins = useActivatedPluginsSNSAdaptor()
const i18n = usePluginI18NField()
if (!meta) return null
const metadata = [...meta.entries()]
return (
Expand All @@ -489,21 +492,25 @@ function BadgeRenderer({ meta }: { meta: TypedMessage['meta'] }) {
if (!render) return null

if (typeof render === 'function') {
if (process.env.NODE_ENV === 'development')
return normalizeBadgeDescriptor(key, plugin, render(key, value))
if (process.env.NODE_ENV === 'development') {
// crash early in dev
return normalizeBadgeDescriptor(key, plugin, render(key, value), i18n)
}
try {
return normalizeBadgeDescriptor(key, plugin, render(key, value))
return normalizeBadgeDescriptor(key, plugin, render(key, value), i18n)
} catch (e) {
console.error(e)
return null
}
} else {
const f = render.get(key)
if (!f) return null
if (process.env.NODE_ENV === 'development')
return normalizeBadgeDescriptor(key, plugin, f(value))
if (process.env.NODE_ENV === 'development') {
// crash early in dev
return normalizeBadgeDescriptor(key, plugin, f(value), i18n)
}
try {
return normalizeBadgeDescriptor(key, plugin, f(value))
return normalizeBadgeDescriptor(key, plugin, f(value), i18n)
} catch (e) {
console.error(e)
return null
Expand All @@ -518,9 +525,10 @@ function normalizeBadgeDescriptor(
meta: string,
plugin: Plugin.SNSAdaptor.Definition,
desc: Plugin.SNSAdaptor.BadgeDescriptor | string | null,
i18n: (id: string, field: I18NStringField) => string,
) {
if (!desc) return null
if (typeof desc === 'string') desc = { text: desc, tooltip: `Provided by plugin "${plugin.name.fallback}"` }
if (typeof desc === 'string') desc = { text: desc, tooltip: `Provided by plugin "${i18n(plugin.ID, plugin.name)}"` }
return (
<MetaBadge key={meta + ';' + plugin.ID} title={desc.tooltip || ''} meta={meta}>
{desc.text}
Expand All @@ -538,11 +546,7 @@ function MetaBadge({ title, children, meta: key }: React.PropsWithChildren<{ tit
</Box>
)
}
function renderLabel(label: Plugin.SNSAdaptor.CompositionDialogEntry['label']): React.ReactNode {
if (!label) return null
if (typeof label === 'object' && 'fallback' in label) return label.fallback
return label
}

type ExtraPluginProps = { unstable: boolean; id: string }
function PluginKindCustom(props: Plugin.SNSAdaptor.CompositionDialogEntryCustom & ExtraPluginProps) {
const classes = useStyles()
Expand All @@ -552,7 +556,7 @@ function PluginKindCustom(props: Plugin.SNSAdaptor.CompositionDialogEntryCustom
<ClickableChip
label={
<>
{renderLabel(label)}
<PluginI18NFieldRender field={label} pluginID={id} />
{unstable && <sup className={classes.sup}>(Beta)</sup>}
</>
}
Expand All @@ -572,7 +576,7 @@ function PluginKindDialog(props: Plugin.SNSAdaptor.CompositionDialogEntryDialog
<ClickableChip
label={
<>
{renderLabel(label)}
<PluginI18NFieldRender field={label} pluginID={id} />
{unstable && <sup className={classes.sup}>(Beta)</sup>}
</>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PluginCard from '../DashboardComponents/PluginCard'

import DashboardRouterContainer from './Container'
import { useActivatedPluginsSNSAdaptor } from '@masknet/plugin-infra'
import { usePluginI18NField } from '../../../plugin-infra/I18NFieldRender'

const useStyles = makeStyles((theme) => ({
root: {
Expand All @@ -30,6 +31,7 @@ export default function DashboardSettingsRouter() {
const { t } = useI18N()
const classes = useStyles()
const plugins = useActivatedPluginsSNSAdaptor()
const field = usePluginI18NField()

return (
<DashboardRouterContainer title={t('plugins')}>
Expand All @@ -38,10 +40,10 @@ export default function DashboardSettingsRouter() {
<li className={classes.pluginItem} key={plugin.ID}>
<PluginCard
key={plugin.ID}
name={plugin.name.fallback}
name={field(plugin.ID, plugin.name)}
id={plugin.ID}
icon={plugin.icon}
description={plugin.description?.fallback}
description={plugin.description ? field(plugin.ID, plugin.description) : ''}
/>
</li>
))}
Expand Down
43 changes: 43 additions & 0 deletions packages/maskbook/src/plugin-infra/I18NFieldRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { I18NFieldOrReactNode, I18NStringField } from '@masknet/plugin-infra'
import { useTranslation } from 'react-i18next'

export interface PluginI18NFieldRenderProps {
field: I18NFieldOrReactNode
pluginID: string
}
export function PluginI18NFieldRender({ pluginID, field }: PluginI18NFieldRenderProps) {
const [t] = useTranslation()
if (!field) return null
debugger
if (typeof field === 'object' && 'fallback' in field) {
return (
<>
{field.i18nKey
? t(field.i18nKey, { ns: pluginID, nsSeparator: '%%%', defaultValue: field.fallback })
: field.fallback}
</>
)
}
return <>{field}</>
}
export function usePluginI18NField() {
const [t] = useTranslation()
return function (pluginID: string, field: I18NStringField) {
if (!field.i18nKey) return field.fallback
if (!field.i18nKey.startsWith('__')) {
/**
* This field is used in the definition of a plugin in form of
* { fallback: "Text", i18nKey: "name" }
*
* Which is highly not likely to be analyzed by the type system.
* Enforce those key to starts with __, we can exclude those keys
* from the unused key result to keep the functionality of the analyzer.
*/
console.warn(
`[@masknet/plugin-infra] Plugin ${pluginID} uses i18n key ${field.i18nKey}. Please change it to __${field.i18nKey}.`,
)
return field.fallback
}
return t(field.i18nKey, { ns: pluginID, nsSeparator: '%%%', defaultValue: field.fallback })
}
}
9 changes: 7 additions & 2 deletions packages/plugin-infra/src/manager/manage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Emitter } from '@servie/events'
import { ALL_EVENTS } from '@masknet/shared'
import type { Plugin } from '../types'
import { getPluginDefine, registeredPluginIDs } from './store'
import { getPluginDefine, registeredPluginIDs, registeredPlugins } from './store'

interface ActivatedPluginInstance<U extends Plugin.Shared.DefinitionWithInit> {
instance: U
Expand Down Expand Up @@ -40,12 +40,17 @@ export function createManager<T extends Plugin.Shared.DefinitionWithInit>(_: Cre
events,
}

function startDaemon({ enabled, signal }: Plugin.__Host.Host, extraCheck?: (id: string) => boolean) {
function startDaemon(host: Plugin.__Host.Host, extraCheck?: (id: string) => boolean) {
const { enabled, signal, addI18NResource } = host
const off2 = enabled.events.on(ALL_EVENTS, checkRequirementAndStartOrStop)

signal?.addEventListener('abort', () => [...activated.keys()].forEach(stopPlugin))
signal?.addEventListener('abort', off2)

for (const plugin of registeredPlugins) {
plugin.i18n && addI18NResource(plugin.ID, plugin.i18n)
}

checkRequirementAndStartOrStop()
function checkRequirementAndStartOrStop() {
for (const id of registeredPluginIDs) {
Expand Down
5 changes: 3 additions & 2 deletions packages/plugin-infra/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export namespace Plugin.SNSAdaptor {
* A label that will be rendered in the CompositionDialog as a chip.
* @example {fallback: "🧧 Red Packet"}
*/
label: I18NStringField | React.ReactNode
label: I18NFieldOrReactNode
/** This callback will be called when the user clicked on the chip. */
onClick(): void
}
Expand All @@ -202,7 +202,7 @@ export namespace Plugin.SNSAdaptor {
* A label that will be rendered in the CompositionDialog as a chip.
* @example {fallback: "🧧 Red Packet"}
*/
label: I18NStringField | React.ReactNode
label: I18NFieldOrReactNode
/** A React dialog component that receives `open` and `onClose`. The dialog will be opened when the chip clicked. */
dialog: React.ComponentType<CompositionDialogEntry_DialogProps>
/**
Expand Down Expand Up @@ -306,6 +306,7 @@ export interface I18NStringField {
/** The fallback content to display if there is no i18n string found. */
fallback: string
}
export type I18NFieldOrReactNode = I18NStringField | React.ReactNode

/**
* The current running SocialNetwork.
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-infra/src/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function createInjectHooksRenderer<PluginDefinition extends Plugin.Shared
.map(picker)
.filter((x) => x.ui)
.map(({ key, name, ui }) => (
// TODO: i18n
<ErrorBoundary key={key} subject={`Plugin ` + name.fallback}>
<Main UI={ui!} data={props} />
</ErrorBoundary>
Expand Down
8 changes: 4 additions & 4 deletions packages/plugins/example/src/SNSAdaptor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ const sns: Plugin.SNSAdaptor.Definition = {
// SearchBoxComponent: HelloWorld,
// DecryptedInspector: HelloWorld,
// GlobalInjection: GlobalComponent,
// CompositionDialogEntry: {
// label: '🤔 Example',
// onClick: () => alert('It works ™!'),
// },
CompositionDialogEntry: {
label: { i18nKey: '__entry__', fallback: '🤔 Example' },
onClick: () => alert('It works ™!'),
},
// CompositionDialogEntry: {
// label: '🤣 Example Dialog',
// dialog: PluginDialog,
Expand Down
3 changes: 2 additions & 1 deletion packages/plugins/example/src/locales/en.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"name": "Example Plugin"
"name": "Example Plugin",
"__entry__": "🤔 Example"
}
3 changes: 2 additions & 1 deletion packages/plugins/example/src/locales/zh.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"name": "測試插件"
"name": "測試插件",
"__entry__": "🤔 測試"
}

0 comments on commit a5930fd

Please sign in to comment.