From 0e48af99397489528c4e6bd5fca815fc6ade6959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kuchy=C5=88ka=20=28Anty=29?= Date: Tue, 26 May 2026 15:44:58 +0200 Subject: [PATCH] feat: add KeyNamePlugin for msgctxt separator chip Adds a CodeMirror plugin and styled wrapper that render the \u0004 separator used by Tolgee to encode gettext msgctxt in front of msgid inside key names. Exports the plugin, the separator constant, and a generateKeyNameStyle helper so the same chip visual is reused by read-only key name displays in tolgee-platform and tolgee-js. --- src/parser/KeyNamePlugin.ts | 92 +++++++++++++++++++++++++++++++++++++ src/parser/keyNameStyle.ts | 45 ++++++++++++++++++ src/tolgee-editor.ts | 5 ++ 3 files changed, 142 insertions(+) create mode 100644 src/parser/KeyNamePlugin.ts create mode 100644 src/parser/keyNameStyle.ts diff --git a/src/parser/KeyNamePlugin.ts b/src/parser/KeyNamePlugin.ts new file mode 100644 index 0000000..7c5fb28 --- /dev/null +++ b/src/parser/KeyNamePlugin.ts @@ -0,0 +1,92 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, + WidgetType, +} from "@codemirror/view"; +import { Extension, RangeSetBuilder, StateField } from "@codemirror/state"; + +export const PO_MSGCTXT_KEY_SEPARATOR = "\u0004"; + +type MsgCtxtRange = { + start: number; + end: number; + msgctxt: string; +}; + +function findMsgCtxt(text: string): MsgCtxtRange | null { + const idx = text.indexOf(PO_MSGCTXT_KEY_SEPARATOR); + if (idx <= 0) return null; + return { start: 0, end: idx + 1, msgctxt: text.slice(0, idx) }; +} + +class MsgCtxtChipWidget extends WidgetType { + constructor(public msgctxt: string) { + super(); + } + + eq(other: WidgetType): boolean { + return other instanceof MsgCtxtChipWidget && other.msgctxt === this.msgctxt; + } + + toDOM(): HTMLElement { + const span = document.createElement("span"); + span.className = "keyname-msgctxt-widget"; + span.textContent = this.msgctxt; + return span; + } + + ignoreEvent(): boolean { + return false; + } +} + +function buildSet(text: string): DecorationSet { + const builder = new RangeSetBuilder(); + const range = findMsgCtxt(text); + if (range) { + builder.add( + range.start, + range.end, + Decoration.replace({ + widget: new MsgCtxtChipWidget(range.msgctxt), + }) + ); + } + return builder.finish(); +} + +export const KeyNamePlugin = (): StateField => { + return StateField.define({ + create() { + return null; + }, + update() { + return null; + }, + provide(): Extension { + return ViewPlugin.fromClass( + class { + decorationSet: DecorationSet; + constructor(view: EditorView) { + this.decorationSet = buildSet(view.state.doc.toString()); + } + update(change: ViewUpdate) { + if (change.docChanged || change.viewportChanged) { + this.decorationSet = buildSet(change.state.doc.toString()); + } + } + }, + { + decorations: (instance) => instance.decorationSet, + provide: (plugin) => + EditorView.atomicRanges.of((view) => { + return view.plugin(plugin)?.decorationSet || Decoration.none; + }), + } + ); + }, + }); +}; diff --git a/src/parser/keyNameStyle.ts b/src/parser/keyNameStyle.ts new file mode 100644 index 0000000..53b116c --- /dev/null +++ b/src/parser/keyNameStyle.ts @@ -0,0 +1,45 @@ +import { StyledComponent } from "@emotion/styled"; + +export type KeyNameChipColors = { + border: string; + background: string; + text: string; +}; + +const DEFAULT_COLORS: KeyNameChipColors = { + border: "#BBC2CB", + background: "#F0F2F4", + text: "#4D5B6E", +}; + +type Props = { + styled: (component: any) => any; + colors?: KeyNameChipColors; + component?: any; +}; + +export const generateKeyNameStyle = ({ + styled, + colors = DEFAULT_COLORS, + component = "div", +}: Props): StyledComponent => { + return styled(component)` + & .keyname-msgctxt-widget { + display: inline-flex; + vertical-align: text-top; + align-items: center; + justify-content: center; + min-width: 15px; + border: 1px solid ${colors.border}; + background-color: ${colors.background}; + color: ${colors.text}; + border-radius: 10px; + padding: 0px 7px; + font-size: 12px; + user-select: none; + margin: 0px 4px 0px 1px; + overflow: hidden; + font-family: monospace; + } + `; +}; diff --git a/src/tolgee-editor.ts b/src/tolgee-editor.ts index 7fafc39..2328f4c 100644 --- a/src/tolgee-editor.ts +++ b/src/tolgee-editor.ts @@ -14,3 +14,8 @@ export { getPluralVariants, getVariantExample } from "./utils/pluralTools"; export { getLanguageDirection } from "./utils/getLanguageDirection"; export { getPluralRules, selectPluralRule } from "./utils/plurals"; export * from "./parser/types"; +export { + KeyNamePlugin, + PO_MSGCTXT_KEY_SEPARATOR, +} from "./parser/KeyNamePlugin"; +export { generateKeyNameStyle } from "./parser/keyNameStyle";