diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md
index 1ca0660f..2757b78a 100644
--- a/docs/API_REFERENCE.md
+++ b/docs/API_REFERENCE.md
@@ -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
+
+```
+
+See [COPY_OPTIONS.md](./COPY_OPTIONS.md#localizing-menu-labels) for details.
+
---
## EnrichedMarkdownTextInput
diff --git a/docs/COPY_OPTIONS.md b/docs/COPY_OPTIONS.md
index 0ca0f71d..9ec727dc 100644
--- a/docs/COPY_OPTIONS.md
+++ b/docs/COPY_OPTIONS.md
@@ -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
+
+```
+
+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.
diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
index 39f034d1..7cf012d4 100644
--- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
+++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt
@@ -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)
}
@@ -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)
resolvedClass.getMethod("applyLatex", String::class.java).invoke(view, segment.latex)
view
} catch (e: Exception) {
diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt
index a301f915..68defbb1 100644
--- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt
+++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt
@@ -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") ?: "",
)
}
diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
index b457f965..be63e10f 100644
--- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
+++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt
@@ -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 = "",
)
/**
@@ -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) {
@@ -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)
}
diff --git a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt
index 8ce6f79a..9a4e4396 100644
--- a/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt
+++ b/packages/react-native-enriched-markdown/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt
@@ -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
@@ -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
@@ -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))
}
}
diff --git a/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt b/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt
index 6383c480..20baf9af 100644
--- a/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt
+++ b/packages/react-native-enriched-markdown/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt
@@ -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()
@@ -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$$"))
}
}
diff --git a/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm b/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm
index 83b4f2c8..1256b0f0 100644
--- a/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm
+++ b/packages/react-native-enriched-markdown/ios/EnrichedMarkdown.mm
@@ -106,6 +106,12 @@ @implementation EnrichedMarkdown {
NSArray *_contextMenuItemTexts;
NSArray *_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;
@@ -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;
@@ -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;
}
@@ -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) {
diff --git a/packages/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm b/packages/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm
index 396cd2b1..521db2be 100644
--- a/packages/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm
+++ b/packages/react-native-enriched-markdown/ios/EnrichedMarkdownText.mm
@@ -98,6 +98,12 @@ @implementation EnrichedMarkdownText {
NSArray *_contextMenuItemTexts;
NSArray *_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;
@@ -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) {
diff --git a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils+macOS.m b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils+macOS.m
index 7876f604..88772671 100644
--- a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils+macOS.m
+++ b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils+macOS.m
@@ -7,6 +7,11 @@
#if TARGET_OS_OSX
+static NSString *resolveMenuLabel(NSString *_Nullable label, NSString *fallback)
+{
+ return label.length > 0 ? label : fallback;
+}
+
NSMenu *_Nullable buildEditMenuForSelection(NSAttributedString *attributedText, NSRange range,
NSString *_Nullable cachedMarkdown, StyleConfig *styleConfig,
NSArray *suggestedActions, NSArray *_Nullable customItems,
@@ -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];
@@ -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];
+ }
[menu addItem:ENRMCreateMenuItem(title, ^{
NSString *urlsToCopy = [imageURLs componentsJoinedByString:@"\n"];
copyStringToPasteboard(urlsToCopy);
diff --git a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.h b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.h
index ed17afbf..96dc400f 100644
--- a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.h
+++ b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.h
@@ -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
diff --git a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.m b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.m
index f4852478..ea80a496 100644
--- a/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.m
+++ b/packages/react-native-enriched-markdown/ios/utils/EditMenuUtils.m
@@ -10,9 +10,15 @@
static NSString *const kActionIdentifierCopyMarkdown = @"com.swmansion.enriched.markdown.copyMarkdown";
static NSString *const kActionIdentifierCopyImageURL = @"com.swmansion.enriched.markdown.copyImageURL";
-static UIAction *createCopyAction(NSAttributedString *selectedText, NSString *markdown, StyleConfig *styleConfig)
+static NSString *resolveLabel(NSString *_Nullable label, NSString *fallback)
{
- return [UIAction actionWithTitle:@"Copy"
+ return label.length > 0 ? label : fallback;
+}
+
+static UIAction *createCopyAction(NSAttributedString *selectedText, NSString *markdown, StyleConfig *styleConfig,
+ NSString *_Nullable copyLabel)
+{
+ return [UIAction actionWithTitle:resolveLabel(copyLabel, @"Copy")
image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
identifier:kActionIdentifierCopy
handler:^(__kindof UIAction *action) {
@@ -20,26 +26,33 @@
}];
}
-static UIAction *_Nullable createCopyMarkdownAction(NSString *markdown)
+static UIAction *_Nullable createCopyMarkdownAction(NSString *markdown, NSString *_Nullable copyAsMarkdownLabel)
{
if (markdown.length == 0)
return nil;
- return [UIAction actionWithTitle:@"Copy as Markdown"
+ return [UIAction actionWithTitle:resolveLabel(copyAsMarkdownLabel, @"Copy as Markdown")
image:[RCTUIImage systemImageNamed:@"doc.text"]
identifier:kActionIdentifierCopyMarkdown
handler:^(__kindof UIAction *action) { copyStringToPasteboard(markdown); }];
}
-static UIAction *_Nullable createCopyImageURLAction(NSArray *imageURLs)
+static UIAction *_Nullable createCopyImageURLAction(NSArray *imageURLs, NSString *_Nullable singularLabel,
+ NSString *_Nullable pluralLabelTemplate)
{
if (imageURLs.count == 0)
return nil;
NSString *urlsToCopy = [imageURLs componentsJoinedByString:@"\n"];
- 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 = resolveLabel(singularLabel, @"Copy Image URL");
+ } else if (pluralLabelTemplate.length > 0) {
+ title = [pluralLabelTemplate stringByReplacingOccurrencesOfString:@"{count}"
+ withString:[@(imageURLs.count) stringValue]];
+ } else {
+ title = [NSString stringWithFormat:@"Copy %lu Image URLs", (unsigned long)imageURLs.count];
+ }
return [UIAction actionWithTitle:title
image:[RCTUIImage systemImageNamed:@"link"]
@@ -80,9 +93,15 @@ static void insertOptionalAction(NSMutableArray *array, UIActio
NSString *markdown = markdownForRange(attributedText, range, cachedMarkdown);
NSArray *imageURLs = imageURLsInRange(attributedText, range);
- UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig);
- UIAction *copyMarkdownAction = selectionMenuConfig.copyAsMarkdown ? createCopyMarkdownAction(markdown) : nil;
- UIAction *copyImageURLAction = selectionMenuConfig.copyImageURL ? createCopyImageURLAction(imageURLs) : nil;
+ UIAction *copyAction = createCopyAction(selectedText, markdown, styleConfig, selectionMenuConfig.copyLabel);
+ UIAction *copyMarkdownAction =
+ selectionMenuConfig.copyAsMarkdown ? createCopyMarkdownAction(markdown, selectionMenuConfig.copyAsMarkdownLabel)
+ : nil;
+ UIAction *copyImageURLAction =
+ selectionMenuConfig.copyImageURL
+ ? createCopyImageURLAction(imageURLs, selectionMenuConfig.copyImageUrlLabel,
+ selectionMenuConfig.copyImageUrlsLabel)
+ : nil;
NSMutableArray *result = [NSMutableArray array];
BOOL foundStandardEditMenu = NO;
diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h b/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h
index 87e8f8ec..94e2f876 100644
--- a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h
+++ b/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.h
@@ -15,6 +15,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) StyleConfig *config;
@property (nonatomic, copy, readonly) NSString *cachedLatex;
+// Localized labels for the copy menu. Empty/nil means "use the English default".
+@property (nonatomic, copy, nullable) NSString *copyLabel;
+@property (nonatomic, copy, nullable) NSString *copyAsMarkdownLabel;
+
@end
NS_ASSUME_NONNULL_END
diff --git a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m b/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m
index 55f63561..e58d45f9 100644
--- a/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m
+++ b/packages/react-native-enriched-markdown/ios/views/ENRMMathContainerView.m
@@ -119,13 +119,14 @@ - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction
previewProvider:nil
actionProvider:^UIMenu *(NSArray *suggestedActions) {
UIAction *copyPlainText =
- [UIAction actionWithTitle:@"Copy"
+ [UIAction actionWithTitle:self.copyLabel.length > 0 ? self.copyLabel : @"Copy"
image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
identifier:nil
handler:^(__kindof UIAction *action) { [self copyLatexToPasteboard]; }];
UIAction *copyMarkdown =
- [UIAction actionWithTitle:@"Copy as Markdown"
+ [UIAction actionWithTitle:self.copyAsMarkdownLabel.length > 0 ? self.copyAsMarkdownLabel
+ : @"Copy as Markdown"
image:[RCTUIImage systemImageNamed:@"doc.text"]
identifier:nil
handler:^(__kindof UIAction *action) { [self copyMarkdownToPasteboard]; }];
@@ -139,8 +140,11 @@ - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction
- (NSMenu *)menuForEvent:(NSEvent *)event
{
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
- [menu addItem:ENRMCreateMenuItem(NSLocalizedString(@"Copy", nil), ^{ [self copyLatexToPasteboard]; })];
- [menu addItem:ENRMCreateMenuItem(NSLocalizedString(@"Copy as Markdown", nil), ^{ [self copyMarkdownToPasteboard]; })];
+ NSString *copyTitle = self.copyLabel.length > 0 ? self.copyLabel : NSLocalizedString(@"Copy", nil);
+ NSString *copyMarkdownTitle =
+ self.copyAsMarkdownLabel.length > 0 ? self.copyAsMarkdownLabel : NSLocalizedString(@"Copy as Markdown", nil);
+ [menu addItem:ENRMCreateMenuItem(copyTitle, ^{ [self copyLatexToPasteboard]; })];
+ [menu addItem:ENRMCreateMenuItem(copyMarkdownTitle, ^{ [self copyMarkdownToPasteboard]; })];
return menu;
}
#endif
diff --git a/packages/react-native-enriched-markdown/ios/views/TableContainerView.h b/packages/react-native-enriched-markdown/ios/views/TableContainerView.h
index 434075fc..6c95e058 100644
--- a/packages/react-native-enriched-markdown/ios/views/TableContainerView.h
+++ b/packages/react-native-enriched-markdown/ios/views/TableContainerView.h
@@ -30,6 +30,10 @@ typedef void (^TableLinkPressBlock)(NSString *url);
@property (nonatomic, assign) ENRMWritingDirectionMode writingDirectionMode;
@property (nonatomic, assign) NSWritingDirection resolvedLayoutDirection;
+// Localized labels for the copy menu. Empty/nil means "use the English default".
+@property (nonatomic, copy, nullable) NSString *copyLabel;
+@property (nonatomic, copy, nullable) NSString *copyAsMarkdownLabel;
+
@property (nonatomic, readonly) NSUInteger rowCount;
- (void)animateNewRowsFromPreviousCount:(NSUInteger)previousRowCount duration:(NSTimeInterval)duration;
diff --git a/packages/react-native-enriched-markdown/ios/views/TableContainerView.m b/packages/react-native-enriched-markdown/ios/views/TableContainerView.m
index d4600090..a6186c6a 100644
--- a/packages/react-native-enriched-markdown/ios/views/TableContainerView.m
+++ b/packages/react-native-enriched-markdown/ios/views/TableContainerView.m
@@ -86,9 +86,12 @@ - (void)setupScrollView
if (!strongSelf)
return nil;
NSMenu *menu = [[NSMenu alloc] initWithTitle:@""];
- [menu addItem:ENRMCreateMenuItem(NSLocalizedString(@"Copy", nil), ^{ [strongSelf copyTableToPasteboard]; })];
- [menu addItem:ENRMCreateMenuItem(NSLocalizedString(@"Copy as Markdown", nil),
- ^{ [strongSelf copyMarkdownToPasteboard]; })];
+ NSString *copyTitle = strongSelf.copyLabel.length > 0 ? strongSelf.copyLabel : NSLocalizedString(@"Copy", nil);
+ NSString *copyMarkdownTitle =
+ strongSelf.copyAsMarkdownLabel.length > 0 ? strongSelf.copyAsMarkdownLabel
+ : NSLocalizedString(@"Copy as Markdown", nil);
+ [menu addItem:ENRMCreateMenuItem(copyTitle, ^{ [strongSelf copyTableToPasteboard]; })];
+ [menu addItem:ENRMCreateMenuItem(copyMarkdownTitle, ^{ [strongSelf copyMarkdownToPasteboard]; })];
return menu;
};
_gridContainer = gridView;
@@ -478,13 +481,14 @@ - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction
previewProvider:nil
actionProvider:^UIMenu *(NSArray *suggestedActions) {
UIAction *copyMarkdown =
- [UIAction actionWithTitle:@"Copy as Markdown"
+ [UIAction actionWithTitle:self.copyAsMarkdownLabel.length > 0 ? self.copyAsMarkdownLabel
+ : @"Copy as Markdown"
image:[RCTUIImage systemImageNamed:@"doc.text"]
identifier:nil
handler:^(__kindof UIAction *action) { [self copyMarkdownToPasteboard]; }];
UIAction *copyPlainText =
- [UIAction actionWithTitle:@"Copy"
+ [UIAction actionWithTitle:self.copyLabel.length > 0 ? self.copyLabel : @"Copy"
image:[RCTUIImage systemImageNamed:@"doc.on.doc"]
identifier:nil
handler:^(__kindof UIAction *action) { [self copyTableToPasteboard]; }];
diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts b/packages/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts
index af74612f..6368be09 100644
--- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts
+++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownNativeComponent.ts
@@ -230,6 +230,13 @@ export interface ContextMenuItemConfig {
export interface SelectionMenuConfig {
copyAsMarkdown: boolean;
copyImageUrl: boolean;
+ // Localizable labels for the built-in selection menu actions. An empty
+ // string means "use the built-in English default". `copyImageUrlsLabel`
+ // is a template where the `{count}` token is replaced by the image count.
+ copyLabel: string;
+ copyAsMarkdownLabel: string;
+ copyImageUrlLabel: string;
+ copyImageUrlsLabel: string;
}
export interface OnContextMenuItemPressEvent {
diff --git a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts
index e84b37ed..61c1508c 100644
--- a/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts
+++ b/packages/react-native-enriched-markdown/src/EnrichedMarkdownTextNativeComponent.ts
@@ -230,6 +230,13 @@ export interface ContextMenuItemConfig {
export interface SelectionMenuConfig {
copyAsMarkdown: boolean;
copyImageUrl: boolean;
+ // Localizable labels for the built-in selection menu actions. An empty
+ // string means "use the built-in English default". `copyImageUrlsLabel`
+ // is a template where the `{count}` token is replaced by the image count.
+ copyLabel: string;
+ copyAsMarkdownLabel: string;
+ copyImageUrlLabel: string;
+ copyImageUrlsLabel: string;
}
export interface OnContextMenuItemPressEvent {
diff --git a/packages/react-native-enriched-markdown/src/index.tsx b/packages/react-native-enriched-markdown/src/index.tsx
index e4d315ec..32c288ae 100644
--- a/packages/react-native-enriched-markdown/src/index.tsx
+++ b/packages/react-native-enriched-markdown/src/index.tsx
@@ -6,6 +6,7 @@ export type {
Md4cFlags,
ContextMenuItem as TextContextMenuItem,
SelectionMenuConfig as TextSelectionMenuConfig,
+ SelectionMenuLabels as TextSelectionMenuLabels,
} from './native/EnrichedMarkdownText';
export type {
LinkPressEvent,
diff --git a/packages/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx b/packages/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx
index 62191980..5a147dc4 100644
--- a/packages/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx
+++ b/packages/react-native-enriched-markdown/src/native/EnrichedMarkdownText.tsx
@@ -10,6 +10,7 @@ import type {
StreamingConfig,
ContextMenuItem,
SelectionMenuConfig,
+ SelectionMenuLabels,
} from '../types/MarkdownTextProps';
import type {
LinkPressEvent,
@@ -24,6 +25,7 @@ export type {
StreamingConfig,
ContextMenuItem,
SelectionMenuConfig,
+ SelectionMenuLabels,
};
export type { LinkPressEvent, LinkLongPressEvent, TaskListItemPressEvent };
@@ -54,6 +56,7 @@ export const EnrichedMarkdownText = ({
spoilerOverlay = 'particles',
contextMenuItems,
selectionMenuConfig,
+ selectionMenuLabels,
selectionColor,
selectionHandleColor,
textBreakStrategy,
@@ -146,8 +149,13 @@ export const EnrichedMarkdownText = ({
() => ({
copyAsMarkdown: selectionMenuConfig?.copyAsMarkdown ?? true,
copyImageUrl: selectionMenuConfig?.copyImageUrl ?? true,
+ // Empty string is the sentinel for "use the native English default".
+ copyLabel: selectionMenuLabels?.copy ?? '',
+ copyAsMarkdownLabel: selectionMenuLabels?.copyAsMarkdown ?? '',
+ copyImageUrlLabel: selectionMenuLabels?.copyImageUrl ?? '',
+ copyImageUrlsLabel: selectionMenuLabels?.copyImageUrls ?? '',
}),
- [selectionMenuConfig]
+ [selectionMenuConfig, selectionMenuLabels]
);
const sharedProps = {
diff --git a/packages/react-native-enriched-markdown/src/types/MarkdownTextProps.ts b/packages/react-native-enriched-markdown/src/types/MarkdownTextProps.ts
index efe70184..189a872a 100644
--- a/packages/react-native-enriched-markdown/src/types/MarkdownTextProps.ts
+++ b/packages/react-native-enriched-markdown/src/types/MarkdownTextProps.ts
@@ -33,6 +33,44 @@ export interface SelectionMenuConfig {
copyImageUrl?: boolean;
}
+/**
+ * Localized labels for the built-in selection/copy menu actions.
+ *
+ * By default these actions are shown in English ("Copy", "Copy as Markdown",
+ * "Copy Image URL"). Pass translated strings to match the rest of your app's
+ * UI — typically wired to your i18n library, e.g.
+ * `selectionMenuLabels={{ copy: t('copy'), copyAsMarkdown: t('copyAsMarkdown') }}`.
+ *
+ * Any label left `undefined` falls back to its built-in English default, so you
+ * can override only the strings you need. Applies to the main text selection
+ * menu as well as the table and math block copy menus.
+ *
+ * @platform ios, android, macos
+ */
+export interface SelectionMenuLabels {
+ /**
+ * Label for the "Copy" action (also used by the table and math copy menus).
+ * @default "Copy"
+ */
+ copy?: string;
+ /**
+ * Label for the "Copy as Markdown" action.
+ * @default "Copy as Markdown"
+ */
+ copyAsMarkdown?: string;
+ /**
+ * Label for the "Copy Image URL" action (single image selected).
+ * @default "Copy Image URL"
+ */
+ copyImageUrl?: string;
+ /**
+ * Label for the "Copy N Image URLs" action (multiple images selected).
+ * The `{count}` token is replaced with the number of selected images.
+ * @default "Copy {count} Image URLs"
+ */
+ copyImageUrls?: string;
+}
+
export interface StreamingConfig {
/**
* Controls how incomplete tables are handled during streaming.
@@ -206,6 +244,15 @@ export interface EnrichedMarkdownTextProps extends Omit {
* @platform ios, android, macos
*/
selectionMenuConfig?: SelectionMenuConfig;
+ /**
+ * 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`.
+ * @platform ios, android, macos
+ */
+ selectionMenuLabels?: SelectionMenuLabels;
/**
* Sets the text direction on the root container.
* Useful for RTL languages — CSS logical properties in the renderers