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