Skip to content

Commit 615a249

Browse files
committed
feat(editor-stacks): add internationalization support with English and Chinese translations
- Created English (en_US.yaml) and Chinese (zh_CN.yaml) translation files for the Stacks Editor plugin. - Implemented translation handling in the new translation.go file. - Updated index.ts to include i18n configuration and component integration. - Added info.yaml to define plugin metadata including slug name, type, and version.
1 parent 23b2868 commit 615a249

File tree

12 files changed

+1246
-0
lines changed

12 files changed

+1246
-0
lines changed

editor-stacks/Component.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { FC, useCallback, useEffect, useRef } from "react";
21+
import { useTranslation } from "react-i18next";
22+
23+
import { StacksEditor } from "@stackoverflow/stacks-editor";
24+
25+
import "@stackoverflow/stacks";
26+
import "@stackoverflow/stacks/dist/css/stacks.css";
27+
28+
import "@stackoverflow/stacks-editor/dist/styles.css";
29+
30+
export interface EditorProps {
31+
value: string;
32+
onChange?: (value: string) => void;
33+
onFocus?: () => void;
34+
onBlur?: () => void;
35+
placeholder?: string;
36+
autoFocus?: boolean;
37+
imageUploadHandler?: (file: File | string) => Promise<string>;
38+
uploadConfig?: {
39+
maxImageSizeMiB?: number;
40+
allowedExtensions?: string[];
41+
};
42+
}
43+
44+
const Component: FC<EditorProps> = ({
45+
value,
46+
onChange,
47+
onFocus,
48+
onBlur,
49+
placeholder = "",
50+
autoFocus = false,
51+
imageUploadHandler,
52+
uploadConfig,
53+
}) => {
54+
const { t } = useTranslation("plugin", {
55+
keyPrefix: "editor_stacks.frontend",
56+
});
57+
const containerRef = useRef<HTMLDivElement>(null);
58+
const editorInstanceRef = useRef<StacksEditor | null>(null);
59+
const isInitializedRef = useRef(false);
60+
61+
// Version compatibility temporarily disabled
62+
63+
const syncTheme = useCallback(() => {
64+
if (!containerRef.current) return;
65+
66+
containerRef.current.parentElement?.classList.remove(
67+
"theme-light",
68+
"theme-dark",
69+
"theme-system",
70+
);
71+
const themeAttr =
72+
document.documentElement.getAttribute("data-bs-theme") ||
73+
document.body.getAttribute("data-bs-theme");
74+
75+
if (themeAttr) {
76+
containerRef.current.parentElement?.classList.add(`theme-system`);
77+
}
78+
}, []);
79+
80+
useEffect(() => {
81+
syncTheme();
82+
}, [syncTheme]);
83+
84+
useEffect(() => {
85+
const observer = new MutationObserver(() => {
86+
syncTheme();
87+
});
88+
observer.observe(document.documentElement, {
89+
attributes: true,
90+
attributeFilter: ["data-bs-theme", "class"],
91+
});
92+
observer.observe(document.body, {
93+
attributes: true,
94+
attributeFilter: ["data-bs-theme", "class"],
95+
});
96+
return () => observer.disconnect();
97+
}, [syncTheme]);
98+
99+
useEffect(() => {
100+
if (!containerRef.current || isInitializedRef.current) {
101+
return;
102+
}
103+
104+
const container = document.createElement("div");
105+
container.className = "stacks-editor-container";
106+
container.style.minHeight = "320px";
107+
containerRef.current.appendChild(container);
108+
109+
let editorInstance: StacksEditor | null = null;
110+
111+
try {
112+
editorInstance = new StacksEditor(container, value || "", {
113+
placeholderText: placeholder || t("placeholder", ""),
114+
parserFeatures: {
115+
tables: true,
116+
html: false,
117+
},
118+
imageUpload: imageUploadHandler
119+
? {
120+
handler: imageUploadHandler,
121+
sizeLimitMib: uploadConfig?.maxImageSizeMiB,
122+
acceptedFileTypes: uploadConfig?.allowedExtensions,
123+
}
124+
: undefined,
125+
});
126+
127+
editorInstanceRef.current = editorInstance;
128+
isInitializedRef.current = true;
129+
130+
const editor = editorInstance;
131+
132+
const originalDispatch = editor.editorView.props.dispatchTransaction;
133+
editor.editorView.setProps({
134+
dispatchTransaction: (tr) => {
135+
if (originalDispatch) {
136+
originalDispatch.call(editor.editorView, tr);
137+
} else {
138+
const newState = editor.editorView.state.apply(tr);
139+
editor.editorView.updateState(newState);
140+
}
141+
142+
if (tr.docChanged && onChange) {
143+
const newContent = editor.content;
144+
onChange(newContent);
145+
}
146+
},
147+
});
148+
149+
const editorElement = editor.dom as HTMLElement;
150+
if (editorElement) {
151+
const handleFocus = () => onFocus?.();
152+
const handleBlur = () => onBlur?.();
153+
editorElement.addEventListener("focus", handleFocus, true);
154+
editorElement.addEventListener("blur", handleBlur, true);
155+
}
156+
157+
if (autoFocus) {
158+
setTimeout(() => {
159+
if (editor) {
160+
editor.focus();
161+
}
162+
}, 100);
163+
}
164+
165+
return () => {
166+
if (editorInstance) {
167+
try {
168+
editorInstance.destroy();
169+
} catch (e) {
170+
console.error("Error destroying editor:", e);
171+
}
172+
}
173+
174+
editorInstanceRef.current = null;
175+
isInitializedRef.current = false;
176+
177+
try {
178+
container.remove();
179+
} catch {}
180+
};
181+
} catch (error) {
182+
console.error("Failed to initialize Stacks Editor:", error);
183+
isInitializedRef.current = false;
184+
}
185+
}, []);
186+
187+
useEffect(() => {
188+
const editor = editorInstanceRef.current;
189+
if (!editor || !isInitializedRef.current) {
190+
return;
191+
}
192+
193+
try {
194+
if (editor.content !== value) {
195+
editor.content = value;
196+
}
197+
} catch (error) {
198+
console.error("Error syncing editor content:", error);
199+
}
200+
}, [value]);
201+
202+
return (
203+
<div className="editor-stacks-wrapper editor-stacks-scope">
204+
<div ref={containerRef} style={{ minHeight: 320 }} />
205+
</div>
206+
);
207+
};
208+
209+
export default Component;

editor-stacks/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Editor-Stacks Plugin
2+
3+
A complete editor replacement plugin for Answer, built on [StackExchange/Stacks-Editor](https://github.com/StackExchange/Stacks-Editor).
4+
5+
## Features
6+
7+
-**Complete Editor Replacement**: Replaces Answer's default Markdown editor with a rich text editor experience
8+
- 📝 **Markdown Support**: Seamless switching between rich text and Markdown editing modes
9+
- 🎨 **Stack Overflow Style**: Built with the Stacks Design System for a familiar and professional look
10+
- 🖼️ **Image Upload**: Supports drag-and-drop, paste from clipboard, and file picker for image uploads
11+
- 📊 **Table Support**: Visual table editing with full Markdown table compatibility
12+
- 🔧 **Rich Formatting**: Code blocks, blockquotes, lists, headings, bold, italic, and more
13+
- ⌨️ **Keyboard Shortcuts**: Full keyboard support for power users
14+
- 🌓 **Theme Sync**: Automatically adapts to Answer's light/dark theme settings
15+
16+
## Plugin Type
17+
18+
This plugin uses the `editor_replacement` type, which means:
19+
- Only one editor replacement plugin can be active at a time
20+
- It completely replaces the default editor interface
21+
- All editor functionality is provided by the plugin
22+
23+
## Related Links
24+
25+
- [Stacks-Editor Documentation](https://github.com/StackExchange/Stacks-Editor)
26+
- [Stacks Design System](https://stackoverflow.design/)
27+
28+

editor-stacks/editor_stacks.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package editor_stacks
21+
22+
import (
23+
"embed"
24+
25+
"github.com/apache/answer-plugins/editor-stacks/i18n"
26+
"github.com/apache/answer-plugins/util"
27+
"github.com/apache/answer/plugin"
28+
)
29+
30+
//go:embed info.yaml
31+
var Info embed.FS
32+
33+
type EditorStacksPlugin struct {
34+
}
35+
36+
func init() {
37+
plugin.Register(&EditorStacksPlugin{})
38+
}
39+
40+
func (d EditorStacksPlugin) Info() plugin.Info {
41+
info := &util.Info{}
42+
info.GetInfo(Info)
43+
44+
return plugin.Info{
45+
Name: plugin.MakeTranslator(i18n.InfoName),
46+
SlugName: info.SlugName,
47+
Description: plugin.MakeTranslator(i18n.InfoDescription),
48+
Author: info.Author,
49+
Version: info.Version,
50+
Link: info.Link,
51+
}
52+
}

editor-stacks/global.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module '*.css?raw' {
2+
const content: string;
3+
export default content;
4+
}

editor-stacks/go.mod

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module github.com/apache/answer-plugins/editor-stacks
2+
3+
go 1.23.0
4+
5+
require (
6+
github.com/apache/answer v1.7.0
7+
github.com/apache/answer-plugins/util v1.0.3-0.20250107030257-cf94ebc70954
8+
)
9+
10+
require (
11+
github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect
12+
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
13+
github.com/aymerick/douceur v0.2.0 // indirect
14+
github.com/bytedance/sonic v1.12.2 // indirect
15+
github.com/bytedance/sonic/loader v0.2.0 // indirect
16+
github.com/cloudwego/base64x v0.1.4 // indirect
17+
github.com/cloudwego/iasm v0.2.0 // indirect
18+
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
19+
github.com/gin-contrib/sse v0.1.0 // indirect
20+
github.com/gin-gonic/gin v1.10.0 // indirect
21+
github.com/go-playground/locales v0.14.1 // indirect
22+
github.com/go-playground/universal-translator v0.18.1 // indirect
23+
github.com/go-playground/validator/v10 v10.22.1 // indirect
24+
github.com/goccy/go-json v0.10.3 // indirect
25+
github.com/golang/snappy v0.0.4 // indirect
26+
github.com/google/wire v0.5.0 // indirect
27+
github.com/gorilla/css v1.0.1 // indirect
28+
github.com/json-iterator/go v1.1.12 // indirect
29+
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
30+
github.com/leodido/go-urn v1.4.0 // indirect
31+
github.com/mattn/go-isatty v0.0.20 // indirect
32+
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
33+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
34+
github.com/modern-go/reflect2 v1.0.2 // indirect
35+
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
36+
github.com/segmentfault/pacman v1.0.5-0.20230822083413-c0075a2d401f // indirect
37+
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20230822083413-c0075a2d401f // indirect
38+
github.com/syndtr/goleveldb v1.0.0 // indirect
39+
github.com/tidwall/gjson v1.17.3 // indirect
40+
github.com/tidwall/match v1.1.1 // indirect
41+
github.com/tidwall/pretty v1.2.1 // indirect
42+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
43+
github.com/ugorji/go/codec v1.2.12 // indirect
44+
github.com/yuin/goldmark v1.7.4 // indirect
45+
golang.org/x/arch v0.10.0 // indirect
46+
golang.org/x/crypto v0.36.0 // indirect
47+
golang.org/x/net v0.38.0 // indirect
48+
golang.org/x/sys v0.31.0 // indirect
49+
golang.org/x/text v0.23.0 // indirect
50+
google.golang.org/protobuf v1.34.2 // indirect
51+
gopkg.in/yaml.v3 v3.0.1 // indirect
52+
sigs.k8s.io/yaml v1.4.0 // indirect
53+
xorm.io/builder v0.3.13 // indirect
54+
xorm.io/xorm v1.3.2 // indirect
55+
)

0 commit comments

Comments
 (0)