Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,45 @@ interface SelectionMenuConfig {

> **Note:** When using `flavor="github"`, `selection.start` and `selection.end` are relative to the text segment the selection is in, not the full markdown string. With `flavor="commonmark"` (default) they are always absolute within the full rendered text.

### `selectionMenuLabels`

Localized labels for the built-in selection/copy menu actions. Use this to translate **Copy**, **Copy as Markdown** and **Copy Image URL** so they match the rest of your app's UI. Any label left `undefined` keeps its English default. Controls which items are shown with `selectionMenuConfig`.

| Type | Default Value | Platform |
| --------------------- | ------------- | ------------------- |
| `SelectionMenuLabels` | `undefined` | iOS, Android, macOS |

**`SelectionMenuLabels` shape:**

```ts
interface SelectionMenuLabels {
/** Label for the "Copy" action (also used by table/math copy menus). @default "Copy" */
copy?: string;
/** Label for the "Copy as Markdown" action. @default "Copy as Markdown" */
copyAsMarkdown?: string;
/** Label for the single-image "Copy Image URL" action. @default "Copy Image URL" */
copyImageUrl?: string;
/** Multi-image label; `{count}` is replaced by the number of selected images. @default "Copy {count} Image URLs" */
copyImageUrls?: string;
}
```

**Example:**

```tsx
<EnrichedMarkdownText
markdown={content}
selectionMenuLabels={{
copy: t('copy'),
copyAsMarkdown: t('copyAsMarkdown'),
copyImageUrl: t('copyImageUrl'),
copyImageUrls: t('copyImageUrls'),
}}
/>
```

See [COPY_OPTIONS.md](./COPY_OPTIONS.md#localizing-menu-labels) for details.

---

## EnrichedMarkdownTextInput
Expand Down
34 changes: 34 additions & 0 deletions docs/COPY_OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,37 @@ Use `selectionMenuConfig` to hide built-in selection menu actions while keeping
}}
/>
```

## Localizing Menu Labels

The built-in copy actions are shown in English by default (**Copy**, **Copy as
Markdown**, **Copy Image URL**). Use `selectionMenuLabels` to translate them so
they match the rest of your app's UI — typically wired to your i18n library:

```tsx
<EnrichedMarkdownText
markdown={content}
selectionMenuLabels={{
copy: t('copy'), // "Copia"
copyAsMarkdown: t('copyAsMarkdown'), // "Copia come Markdown"
copyImageUrl: t('copyImageUrl'), // "Copia URL immagine"
copyImageUrls: t('copyImageUrls'), // "Copia {count} URL immagine"
}}
/>
```

Notes:

- Any label left `undefined` keeps its English default, so you can override only
the strings you need.
- `copyImageUrls` is the label used when several images are selected; the
`{count}` token is replaced by the number of selected images.
- The labels apply to the main text selection menu as well as the table and math
block copy menus.
- The system **Copy** item on iOS/Android and OS-provided actions (Look Up,
Translate…) are already localized by the platform and are not affected.
- Applies to `EnrichedMarkdownText`. On `EnrichedMarkdownTextInput` only the
visibility config (`selectionMenuConfig`) is available for now.

> The simplest way to keep these in sync with the device language is to feed the
> same translation function you already use for the rest of your UI.
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,8 @@ class EnrichedMarkdown
maxFontSizeMultiplier = this@EnrichedMarkdown.maxFontSizeMultiplier
onLinkPress = onLinkPressCallback
onLinkLongPress = onLinkLongPressCallback
copyLabel = this@EnrichedMarkdown.selectionMenuConfig.copyLabel
copyAsMarkdownLabel = this@EnrichedMarkdown.selectionMenuConfig.copyAsMarkdownLabel
applyTableNode(segment.node)
}

Expand All @@ -477,6 +479,10 @@ class EnrichedMarkdown
resolvedClass
.getConstructor(Context::class.java, StyleConfig::class.java)
.newInstance(context, style) as View
resolvedClass.getMethod("setCopyLabel", String::class.java)
.invoke(view, selectionMenuConfig.copyLabel)
resolvedClass.getMethod("setCopyAsMarkdownLabel", String::class.java)
.invoke(view, selectionMenuConfig.copyAsMarkdownLabel)
Comment on lines 466 to +485

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this code is missing a assign in the prop change section. We need to update everything in case of re-update

For example if someone updates a prop (customer changes language on page without a remount), they would get wrong language
Look at EnrichedMarkdown.kt 229-235 for an example

resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
view
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,9 @@ fun parseSelectionMenuConfig(value: ReadableMap?): SelectionMenuConfig {
return SelectionMenuConfig(
copyAsMarkdown = value.getBoolean("copyAsMarkdown"),
copyImageUrl = value.getBoolean("copyImageUrl"),
copyLabel = value.getString("copyLabel") ?: "",
copyAsMarkdownLabel = value.getString("copyAsMarkdownLabel") ?: "",
copyImageUrlLabel = value.getString("copyImageUrlLabel") ?: "",
copyImageUrlsLabel = value.getString("copyImageUrlsLabel") ?: "",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ private const val MENU_ITEM_CUSTOM_GROUP = 2001
data class SelectionMenuConfig(
val copyAsMarkdown: Boolean = true,
val copyImageUrl: Boolean = true,
// Localized labels. An empty string means "use the built-in English default".
// `copyImageUrlsLabel` is a template where `{count}` is replaced by the count.
val copyLabel: String = "",
val copyAsMarkdownLabel: String = "",
val copyImageUrlLabel: String = "",
val copyImageUrlsLabel: String = "",
)

/**
Expand Down Expand Up @@ -61,7 +67,12 @@ fun createSelectionActionModeCallback(
textView.selectionStart >= 0 &&
textView.selectionEnd > textView.selectionStart
) {
menu.add(Menu.NONE, MENU_ITEM_COPY_MARKDOWN, Menu.NONE, "Copy as Markdown")
menu.add(
Menu.NONE,
MENU_ITEM_COPY_MARKDOWN,
Menu.NONE,
selectionMenuConfig.copyAsMarkdownLabel.ifEmpty { "Copy as Markdown" },
)
}

if (textView.selectionStart >= 0 && textView.selectionEnd > textView.selectionStart) {
Expand All @@ -82,9 +93,11 @@ fun createSelectionActionModeCallback(
if (imageUrls.isNotEmpty()) {
val title =
if (imageUrls.size == 1) {
"Copy Image URL"
selectionMenuConfig.copyImageUrlLabel.ifEmpty { "Copy Image URL" }
} else {
"Copy ${imageUrls.size} Image URLs"
selectionMenuConfig.copyImageUrlsLabel
.ifEmpty { "Copy {count} Image URLs" }
.replace("{count}", imageUrls.size.toString())
}
menu.add(Menu.NONE, MENU_ITEM_COPY_IMAGE_URL, Menu.NONE, title)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class TableContainerView(
var onLinkPress: ((String) -> Unit)? = null
var onLinkLongPress: ((String) -> Unit)? = null

// Localized labels for the copy menu. Empty means "use the English default".
var copyLabel: String = ""
var copyAsMarkdownLabel: String = ""

private val scrollView =
HorizontalScrollView(context).apply {
isHorizontalScrollBarEnabled = true
Expand Down Expand Up @@ -279,7 +283,7 @@ class TableContainerView(
private fun showContextMenu(anchor: View) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
ContextMenuPopup.show(anchor, this) {
item(ContextMenuPopup.Icon.COPY, "Copy") {
item(ContextMenuPopup.Icon.COPY, copyLabel.ifEmpty { "Copy" }) {
val plainText = rows.joinToString("\n") { row -> row.joinToString("\t") { it.plainText } }
if (plainText.isNotEmpty()) {
val displayMetrics = context.resources.displayMetrics
Expand All @@ -291,7 +295,7 @@ class TableContainerView(
clipboard.setPrimaryClip(ClipData.newHtmlText("Table", plainText, html))
}
}
item(ContextMenuPopup.Icon.DOCUMENT, "Copy as Markdown") {
item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel.ifEmpty { "Copy as Markdown" }) {
if (tableMarkdown.isNotEmpty()) clipboard.setPrimaryClip(ClipData.newPlainText("Table", tableMarkdown))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class MathContainerView(
private val scrollView = HorizontalScrollView(context)
private var cachedLatex: String = ""

// Localized labels for the copy menu. Empty means "use the English default".
// Set reflectively by EnrichedMarkdown (math is an optional module).
var copyLabel: String = ""
var copyAsMarkdownLabel: String = ""

override val segmentMarginTop: Int get() = mathStyle.marginTop.toInt()
override val segmentMarginBottom: Int get() = mathStyle.marginBottom.toInt()

Expand Down Expand Up @@ -95,10 +100,10 @@ class MathContainerView(
private fun showContextMenu(anchor: View) {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
ContextMenuPopup.show(anchor, this) {
item(ContextMenuPopup.Icon.COPY, "Copy") {
item(ContextMenuPopup.Icon.COPY, copyLabel.ifEmpty { "Copy" }) {
clipboard.setPrimaryClip(ClipData.newPlainText("Math", cachedLatex))
}
item(ContextMenuPopup.Icon.DOCUMENT, "Copy as Markdown") {
item(ContextMenuPopup.Icon.DOCUMENT, copyAsMarkdownLabel.ifEmpty { "Copy as Markdown" }) {
clipboard.setPrimaryClip(ClipData.newPlainText("Math", "$$\n$cachedLatex\n$$"))
}
}
Expand Down
20 changes: 20 additions & 0 deletions packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this has the same issues as the kotlin. The props are not being walked down to children on updateProps:

follow the existing iOS prop-update idiom - see pushWritingDirectionToTableSegments at line 386 for the model. Add an equivalent pushSelectionMenuLabelsToSegments covering tables and math, call it from updateProps: after the label ivars are reassigned.

Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ @implementation EnrichedMarkdown {
NSArray<NSString *> *_contextMenuItemTexts;
NSArray<NSString *> *_contextMenuItemIcons;
ENRMSelectionMenuConfig _selectionMenuConfig;
// Strong owners for the selection menu labels referenced (unretained) by
// _selectionMenuConfig. Kept alive for the view's lifetime.
NSString *_copyLabel;
NSString *_copyAsMarkdownLabel;
NSString *_copyImageUrlLabel;
NSString *_copyImageUrlsLabel;

ENRMSpoilerOverlay _spoilerOverlay;

Expand Down Expand Up @@ -599,6 +605,8 @@ - (TableContainerView *)createTableViewForSegment:(ENRMTableSegment *)tableSegme
tableView.enableLinkPreview = _enableLinkPreview;
tableView.writingDirectionMode = _writingDirectionMode;
tableView.resolvedLayoutDirection = _resolvedLayoutDirection;
tableView.copyLabel = _copyLabel;
tableView.copyAsMarkdownLabel = _copyAsMarkdownLabel;

__weak EnrichedMarkdown *weakSelf = self;

Expand Down Expand Up @@ -635,6 +643,8 @@ - (void)updateTableView:(TableContainerView *)view withSegment:(ENRMTableSegment
- (ENRMMathContainerView *)createMathViewForSegment:(ENRMMathSegment *)mathSegment
{
ENRMMathContainerView *mathView = [[ENRMMathContainerView alloc] initWithConfig:_config];
mathView.copyLabel = _copyLabel;
mathView.copyAsMarkdownLabel = _copyAsMarkdownLabel;
[mathView applyLatex:mathSegment.latex];
return mathView;
}
Expand Down Expand Up @@ -787,9 +797,19 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
}

_copyLabel = [[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyLabel.c_str()];
_copyAsMarkdownLabel =
[[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyAsMarkdownLabel.c_str()];
_copyImageUrlLabel = [[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyImageUrlLabel.c_str()];
_copyImageUrlsLabel =
[[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyImageUrlsLabel.c_str()];
_selectionMenuConfig = (ENRMSelectionMenuConfig){
.copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown,
.copyImageURL = newViewProps.selectionMenuConfig.copyImageUrl,
.copyLabel = _copyLabel,
.copyAsMarkdownLabel = _copyAsMarkdownLabel,
.copyImageUrlLabel = _copyImageUrlLabel,
.copyImageUrlsLabel = _copyImageUrlsLabel,
};

if (newViewProps.spoilerOverlay != oldViewProps.spoilerOverlay) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ @implementation EnrichedMarkdownText {
NSArray<NSString *> *_contextMenuItemTexts;
NSArray<NSString *> *_contextMenuItemIcons;
ENRMSelectionMenuConfig _selectionMenuConfig;
// Strong owners for the selection menu labels referenced (unretained) by
// _selectionMenuConfig. Kept alive for the view's lifetime.
NSString *_copyLabel;
NSString *_copyAsMarkdownLabel;
NSString *_copyImageUrlLabel;
NSString *_copyImageUrlsLabel;

ENRMSpoilerOverlayManager *_spoilerManager;

Expand Down Expand Up @@ -533,9 +539,19 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
}

_copyLabel = [[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyLabel.c_str()];
_copyAsMarkdownLabel =
[[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyAsMarkdownLabel.c_str()];
_copyImageUrlLabel = [[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyImageUrlLabel.c_str()];
_copyImageUrlsLabel =
[[NSString alloc] initWithUTF8String:newViewProps.selectionMenuConfig.copyImageUrlsLabel.c_str()];
_selectionMenuConfig = (ENRMSelectionMenuConfig){
.copyAsMarkdown = newViewProps.selectionMenuConfig.copyAsMarkdown,
.copyImageURL = newViewProps.selectionMenuConfig.copyImageUrl,
.copyLabel = _copyLabel,
.copyAsMarkdownLabel = _copyAsMarkdownLabel,
.copyImageUrlLabel = _copyImageUrlLabel,
.copyImageUrlsLabel = _copyImageUrlsLabel,
};

if (newViewProps.streamingAnimation != oldViewProps.streamingAnimation) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

#if TARGET_OS_OSX

static NSString *resolveMenuLabel(NSString *_Nullable label, NSString *fallback)
{
return label.length > 0 ? label : fallback;
}

Comment on lines +10 to +14

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: On macOS, the table and math context menus fall back to NSLocalizedString(@"Copy", nil) / NSLocalizedString(@"Copy as Markdown", nil) (see ENRMMathContainerView.m:143-145 and the TableContainerView.m macOS branch). This helper returns the literal English string instead, so an app shipping its own Localizable.strings gets two different fallback behaviors between the main edit menu and the table/math menus. Suggest wrapping the fallback in NSLocalizedString here so all three macOS menus behave consistently

NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig,
NSArray *suggestedActions, NSArray<NSMenuItem *> *_Nullable customItems,
Expand All @@ -25,8 +30,9 @@

// Replace the system Copy item with our enhanced version (copies RTF/HTML/Markdown).
// This mirrors the iOS behaviour where we replace the standard-edit Copy action.
NSString *copyTitle = resolveMenuLabel(selectionMenuConfig.copyLabel, @"Copy");
NSMenuItem *enhancedCopy =
ENRMCreateMenuItem(@"Copy", ^{ copyAttributedStringToPasteboard(selectedText, markdown, styleConfig); });
ENRMCreateMenuItem(copyTitle, ^{ copyAttributedStringToPasteboard(selectedText, markdown, styleConfig); });
NSInteger systemCopyIndex = [menu indexOfItemWithTarget:nil andAction:@selector(copy:)];
if (systemCopyIndex != NSNotFound) {
[menu removeItemAtIndex:systemCopyIndex];
Expand All @@ -39,13 +45,21 @@
}

if (selectionMenuConfig.copyAsMarkdown && markdown.length > 0) {
[menu addItem:ENRMCreateMenuItem(@"Copy as Markdown", ^{ copyStringToPasteboard(markdown); })];
NSString *copyMarkdownTitle = resolveMenuLabel(selectionMenuConfig.copyAsMarkdownLabel, @"Copy as Markdown");
[menu addItem:ENRMCreateMenuItem(copyMarkdownTitle, ^{ copyStringToPasteboard(markdown); })];
}

if (selectionMenuConfig.copyImageURL && imageURLs.count > 0) {
NSString *title = (imageURLs.count == 1)
? @"Copy Image URL"
: [NSString stringWithFormat:@"Copy %lu Image URLs", (unsigned long)imageURLs.count];
NSString *title;
if (imageURLs.count == 1) {
title = resolveMenuLabel(selectionMenuConfig.copyImageUrlLabel, @"Copy Image URL");
} else if (selectionMenuConfig.copyImageUrlsLabel.length > 0) {
NSString *countString = [@(imageURLs.count) stringValue];
title = [selectionMenuConfig.copyImageUrlsLabel stringByReplacingOccurrencesOfString:@"{count}"
withString:countString];
} else {
title = [NSString stringWithFormat:@"Copy %lu Image URLs", (unsigned long)imageURLs.count];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the comment above in this file

}
[menu addItem:ENRMCreateMenuItem(title, ^{
NSString *urlsToCopy = [imageURLs componentsJoinedByString:@"\n"];
copyStringToPasteboard(urlsToCopy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ NS_ASSUME_NONNULL_BEGIN
typedef struct {
BOOL copyAsMarkdown;
BOOL copyImageURL;
// Localized labels. A nil/empty string means "use the built-in English
// default". `copyImageUrlsLabel` is a template where the `{count}` token is
// replaced by the image count. The owner must keep these strings alive for
// the duration of the call (the view holds them in strong ivars).
__unsafe_unretained NSString *_Nullable copyLabel;
__unsafe_unretained NSString *_Nullable copyAsMarkdownLabel;
__unsafe_unretained NSString *_Nullable copyImageUrlLabel;
__unsafe_unretained NSString *_Nullable copyImageUrlsLabel;
} ENRMSelectionMenuConfig;

#ifdef __cplusplus
Expand Down
Loading