Skip to content

Commit

Permalink
feat: i18n support with i18next compatible API
Browse files Browse the repository at this point in the history
  • Loading branch information
petyosi committed Mar 6, 2024
1 parent 70e1109 commit dcbd5cc
Show file tree
Hide file tree
Showing 29 changed files with 426 additions and 115 deletions.
16 changes: 16 additions & 0 deletions docs/i18n.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: i18n
slug: i18n
position: 105
---

# i18n

The component is localized entirely in English by default, but you can override these localizations via the `translation` prop - a function that has a signature compatible with the `t` function of i18next. You only need to localize parts of the UI that you'll actually be using, there is no need to localize the entirety of the editor unless you need to. If you're using i18next, you can use browse the [`locales` directory in GitHub](https://github.com/mdx-editor/editor) for a default set of translations or use a tool like [i18next Parser](https://github.com/i18next/i18next-parser) to extract them from the source code. If you're using another i18n library, you can use the `translation` prop to pass in your own translations

```tsx

function LocalizedEditor() {
return <MDXEditor translation={(key, defaultValue, interpolations) => { return i18n.t(key, defaultValue, interpolations) }} />
}
```
101 changes: 101 additions & 0 deletions locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"frontmatterEditor": {
"title": "Edit document frontmatter",
"key": "Key",
"value": "Value",
"addEntry": "Add entry"
},
"dialogControls": {
"save": "Save",
"cancel": "Cancel"
},
"uploadImage": {
"uploadInstructions": "Upload an image from your device:",
"addViaUrlInstructions": "Or add an image from an URL:",
"autoCompletePlaceholder": "Select or paste an image src",
"alt": "Alt:",
"title": "Title:"
},
"imageEditor": {
"editImage": "Edit image"
},
"createLink": {
"url": "URL",
"urlPlaceholder": "Select or paste an URL",
"title": "Title",
"saveTooltip": "Set URL",
"cancelTooltip": "Cancel change"
},
"linkPreview": {
"open": "Open {{url}} in new window",
"edit": "Edit link URL",
"copyToClipboard": "Copy to clipboard",
"copied": "Copied!",
"remove": "Remove link"
},
"table": {
"deleteTable": "Delete table",
"columnMenu": "Column menu",
"textAlignment": "Text alignment",
"alignLeft": "Align left",
"alignCenter": "Align center",
"alignRight": "Align right",
"insertColumnLeft": "Insert a column to the left of this one",
"insertColumnRight": "Insert a column to the right of this one",
"deleteColumn": "Delete this column",
"rowMenu": "Row menu",
"insertRowAbove": "Insert a row above this one",
"insertRowBelow": "Insert a row below this one",
"deleteRow": "Delete this row"
},
"toolbar": {
"blockTypes": {
"paragraph": "Paragraph",
"quote": "Quote",
"heading": "Heading {{level}}"
},
"blockTypeSelect": {
"selectBlockTypeTooltip": "Select block type",
"placeholder": "Block type"
},
"removeBold": "Remove bold",
"bold": "Bold",
"removeItalic": "Italic",
"italic": "Italic",
"underline": "Remove underline",
"removeUnderline": "Underline",
"removeInlineCode": "Remove code format",
"inlineCode": "Inline code format",
"link": "Create link",
"richText": "Rich text",
"diffMode": "Diff mode",
"source": "Source mode",
"admonition": "Insert Admonition",
"codeBlock": "Insert Code Block",
"editFrontmatter": "Edit frontmatter",
"insertFrontmatter": "Insert frontmatter",
"image": "Insert image",
"insertSandpack": "Insert Sandpack",
"table": "Insert Table",
"thematicBreak": "Insert thematic break",
"bulletedList": "Bulleted list",
"numberedList": "Numbered list",
"checkList": "Check list",
"deleteSandpack": "Delete this code block",
"undo": "Undo {{shortcut}}",
"redo": "Redo {{shortcut}}"
},
"admonitions": {
"note": "Note",
"tip": "Tip",
"danger": "Danger",
"info": "Info",
"caution": "Caution",
"changeType": "Select admonition type",
"placeholder": "Admonition type"
},
"codeBlock": {
"language": "Code block language",
"selectLanguage": "Select code block language"
}
}
38 changes: 31 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"express": "^4.18.2",
"i18next": "^23.10.0",
"jsdom": "^23.0.1",
"mermaid": "^10.6.1",
"micromark-util-types": "^2.0.0",
Expand Down
18 changes: 17 additions & 1 deletion src/MDXEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCellValues, usePublisher, useRealm } from '@mdxeditor/gurx'
import React from 'react'
import { RealmPlugin, RealmWithPlugins } from './RealmWithPlugins'
import {
Translation,
composerChildren$,
contentEditableClassName$,
corePlugin,
Expand Down Expand Up @@ -95,6 +96,7 @@ const DefaultIcon = React.lazy(() => import('./plugins/core/Icon'))
const IconFallback = () => {
return <svg width="24" height="24" viewBox="0 0 24 24" fill="none" />
}

const defaultIconComponentFor = (name: IconKey) => {
return (
<React.Suspense fallback={<IconFallback />}>
Expand All @@ -103,6 +105,15 @@ const defaultIconComponentFor = (name: IconKey) => {
)
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
function defaultTranslation(key: string, defaultValue: string, interpolations = {}) {
let value = defaultValue
for (const [k, v] of Object.entries(interpolations)) {
value = value.replaceAll(`{{${k}}}`, String(v))
}
return value
}

/**
* The interface for the {@link MDXEditor} object reference.
*
Expand Down Expand Up @@ -280,6 +291,10 @@ export interface MDXEditorProps {
* Set to false if you want to suppress the processing of HTML tags.
*/
suppressHtmlProcessing?: boolean
/**
* Pass your own translation function if you want to localize the editor.
*/
translation?: Translation
}

/**
Expand All @@ -301,7 +316,8 @@ export const MDXEditor = React.forwardRef<MDXEditorMethods, MDXEditorProps>((pro
readOnly: Boolean(props.readOnly),
iconComponentFor: props.iconComponentFor ?? defaultIconComponentFor,
suppressHtmlProcessing: props.suppressHtmlProcessing ?? false,
onError: props.onError ?? noop
onError: props.onError ?? noop,
translation: props.translation ?? defaultTranslation
}),
...(props.plugins || [])
]}
Expand Down
45 changes: 45 additions & 0 deletions src/examples/i18n.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import i18next from 'i18next'
import markdown from './assets/live-demo-contents.md?raw'
import { MDXEditor } from '..'
import { ALL_PLUGINS } from './_boilerplate'

const sl = {
toolbar: {
undo: 'Razveljavi {{shortcut}}',
redo: 'Uveljavi {{shortcut}}',
blockTypeSelect: {
selectBlockTypeTooltip: 'Izberi vrsto bloka',
placeholder: 'Vrsta bloka'
},

blockTypes: {
paragraph: 'Odstavek',
heading: 'Naslov',
quote: 'Citat'
}
}
}

void i18next.init({
lng: 'sl', // if you're using a language detector, do not define the lng option
debug: true,
resources: {
sl: {
translation: sl
}
}
})

export const Example = () => {
return (
<MDXEditor
translation={(key, defaultValue, interpolations) => {
return i18next.t(key, defaultValue, interpolations) as string
}}
markdown={markdown}
onChange={(md) => console.log('change', { md })}
plugins={ALL_PLUGINS}
/>
)
}
16 changes: 14 additions & 2 deletions src/plugins/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createEmptyHistoryState } from '@lexical/react/LexicalHistoryPlugin.js'
import { $isHeadingNode, HeadingTagType } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection'
import { $findMatchingParent, $insertNodeToNearestRoot, $wrapNodeInElement } from '@lexical/utils'
import { Cell, NodeRef, Realm, Signal, filter, map, scan, withLatestFrom } from '@mdxeditor/gurx'
import { Cell, NodeRef, Realm, Signal, filter, map, scan, useCellValue, withLatestFrom } from '@mdxeditor/gurx'
import {
$createParagraphNode,
$getRoot,
Expand Down Expand Up @@ -829,6 +829,12 @@ export const activePlugins$ = Cell<string[]>([])
*/
export const addActivePlugin$ = Appender(activePlugins$)

export type Translation = (key: string, defaultValue: string, interpolations?: Record<string, any>) => string

export const translation$ = Cell<Translation>(() => {
throw new Error('No translation function provided')
})

/** @internal */
export const corePlugin = realmPlugin<{
initialMarkdown: string
Expand All @@ -842,6 +848,7 @@ export const corePlugin = realmPlugin<{
readOnly: boolean
iconComponentFor: (name: IconKey) => React.ReactElement
suppressHtmlProcessing?: boolean
translation: Translation
}>({
init(r, params) {
r.register(createRootEditorSubscription$)
Expand Down Expand Up @@ -872,7 +879,8 @@ export const corePlugin = realmPlugin<{
[toMarkdownOptions$]: params?.toMarkdownOptions,
[autoFocus$]: params?.autoFocus,
[placeholder$]: params?.placeholder,
[readOnly$]: params?.readOnly
[readOnly$]: params?.readOnly,
[translation$]: params?.translation
})

// Use the JSX extension to parse HTML
Expand Down Expand Up @@ -900,3 +908,7 @@ export const corePlugin = realmPlugin<{
realm.singletonSub(markdownErrorSignal$, params?.onError)
}
})

export function useTranslation() {
return useCellValue(translation$)
}
Loading

0 comments on commit dcbd5cc

Please sign in to comment.