diff --git a/change/react-native-windows-cec8f9f9-9e4b-4649-9deb-7908ecefc21e.json b/change/react-native-windows-cec8f9f9-9e4b-4649-9deb-7908ecefc21e.json new file mode 100644 index 00000000000..d474dd2da02 --- /dev/null +++ b/change/react-native-windows-cec8f9f9-9e4b-4649-9deb-7908ecefc21e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Add localization infrastructure for context menu strings in TextInput and Text components", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj index aadb98eff59..ff8a3cc12b2 100644 --- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj +++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj @@ -176,6 +176,7 @@ %(PreprocessorDefinitions) + diff --git a/vnext/Desktop/React.Windows.Desktop.vcxproj b/vnext/Desktop/React.Windows.Desktop.vcxproj index 8b1c4f33bc9..23a426a44e5 100644 --- a/vnext/Desktop/React.Windows.Desktop.vcxproj +++ b/vnext/Desktop/React.Windows.Desktop.vcxproj @@ -200,6 +200,7 @@ + diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index 7974583346e..332e77f279b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -9,7 +9,9 @@ #include #include +#include #include +#include #include #include #include @@ -829,7 +831,7 @@ void ParagraphComponentView::SetSelection(int32_t start, int32_t end) noexcept { } void ParagraphComponentView::ShowContextMenu() noexcept { - HMENU menu = CreatePopupMenu(); + std::unique_ptr, decltype(&DestroyMenu)> menu(CreatePopupMenu(), &DestroyMenu); if (!menu) { return; } @@ -838,28 +840,26 @@ void ParagraphComponentView::ShowContextMenu() noexcept { const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)}; const bool hasText = !utf16Text.empty(); - // Add menu items (1 = Copy, 2 = Select All) - AppendMenuW(menu, MF_STRING | (hasSelection ? 0 : MF_GRAYED), 1, L"Copy"); - AppendMenuW(menu, MF_STRING | (hasText ? 0 : MF_GRAYED), 2, L"Select All"); + auto copyStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_COPY); + auto selectAllStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_SELECT_ALL); + + AppendMenuW(menu.get(), MF_STRING | (hasSelection ? 0 : MF_GRAYED), 1, copyStr.c_str()); + AppendMenuW(menu.get(), MF_STRING | (hasText ? 0 : MF_GRAYED), 2, selectAllStr.c_str()); - // Get cursor position for menu placement POINT cursorPos; GetCursorPos(&cursorPos); const HWND hwnd = GetActiveWindow(); const int cmd = TrackPopupMenu( - menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL); + menu.get(), TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL); if (cmd == 1) { - // Copy CopySelectionToClipboard(); } else if (cmd == 2) { SetSelection(0, static_cast(utf16Text.length())); DrawText(); } - - DestroyMenu(menu); } void ParagraphComponentView::OnKeyDown( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 7d181804e43..398573e8770 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include #include @@ -1900,7 +1902,7 @@ void WindowsTextInputComponentView::OnContextMenuKey( } void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Foundation::Point &position) noexcept { - HMENU menu = CreatePopupMenu(); + std::unique_ptr, decltype(&DestroyMenu)> menu(CreatePopupMenu(), &DestroyMenu); if (!menu) return; @@ -1913,15 +1915,20 @@ void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Founda bool isReadOnly = windowsTextInputProps().editable == false; bool canPaste = !isReadOnly && IsClipboardFormatAvailable(CF_UNICODETEXT); - AppendMenuW(menu, MF_STRING | (hasSelection && !isReadOnly ? 0 : MF_GRAYED), 1, L"Cut"); - AppendMenuW(menu, MF_STRING | (hasSelection ? 0 : MF_GRAYED), 2, L"Copy"); - AppendMenuW(menu, MF_STRING | (canPaste ? 0 : MF_GRAYED), 3, L"Paste"); - AppendMenuW(menu, MF_STRING | (!isEmpty && !isReadOnly ? 0 : MF_GRAYED), 4, L"Select All"); + auto cutStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_CUT); + auto copyStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_COPY); + auto pasteStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_PASTE); + auto selectAllStr = ::Microsoft::ReactNative::GetLocalizedString(IDS_CONTEXT_MENU_SELECT_ALL); + + AppendMenuW(menu.get(), MF_STRING | (hasSelection && !isReadOnly ? 0 : MF_GRAYED), 1, cutStr.c_str()); + AppendMenuW(menu.get(), MF_STRING | (hasSelection ? 0 : MF_GRAYED), 2, copyStr.c_str()); + AppendMenuW(menu.get(), MF_STRING | (canPaste ? 0 : MF_GRAYED), 3, pasteStr.c_str()); + AppendMenuW(menu.get(), MF_STRING | (!isEmpty && !isReadOnly ? 0 : MF_GRAYED), 4, selectAllStr.c_str()); HWND hwnd = GetActiveWindow(); int cmd = TrackPopupMenu( - menu, + menu.get(), TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, static_cast(position.X), static_cast(position.Y), @@ -1929,19 +1936,17 @@ void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Founda hwnd, NULL); - if (cmd == 1) { // Cut + if (cmd == 1) { m_textServices->TxSendMessage(WM_CUT, 0, 0, &res); OnTextUpdated(); - } else if (cmd == 2) { // Copy + } else if (cmd == 2) { m_textServices->TxSendMessage(WM_COPY, 0, 0, &res); - } else if (cmd == 3) { // Paste + } else if (cmd == 3) { m_textServices->TxSendMessage(WM_PASTE, 0, 0, &res); OnTextUpdated(); - } else if (cmd == 4) { // Select All + } else if (cmd == 4) { m_textServices->TxSendMessage(EM_SETSEL, 0, -1, &res); } - - DestroyMenu(menu); } } // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 46f03bf1cbb..c2ff5e33a2c 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -307,6 +307,7 @@ + @@ -372,6 +373,7 @@ + @@ -410,6 +412,10 @@ %(PreprocessorDefinitions) + + + + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index 44030e2c2f2..1496cbc7d51 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -143,6 +143,9 @@ Utils + + Utils + Utils @@ -368,6 +371,12 @@ Utils + + Utils + + + Utils + Utils @@ -520,6 +529,7 @@ + diff --git a/vnext/Microsoft.ReactNative/Resources/StringResourceIds.h b/vnext/Microsoft.ReactNative/Resources/StringResourceIds.h new file mode 100644 index 00000000000..8dbac730774 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Resources/StringResourceIds.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#define IDS_CONTEXT_MENU_CUT 100 +#define IDS_CONTEXT_MENU_COPY 101 +#define IDS_CONTEXT_MENU_PASTE 102 +#define IDS_CONTEXT_MENU_SELECT_ALL 103 diff --git a/vnext/Microsoft.ReactNative/Resources/Strings.rc b/vnext/Microsoft.ReactNative/Resources/Strings.rc new file mode 100644 index 00000000000..a5d61115a55 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Resources/Strings.rc @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "StringResourceIds.h" + +STRINGTABLE +BEGIN + IDS_CONTEXT_MENU_CUT "Cut" + IDS_CONTEXT_MENU_COPY "Copy" + IDS_CONTEXT_MENU_PASTE "Paste" + IDS_CONTEXT_MENU_SELECT_ALL "Select All" +END diff --git a/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.cpp b/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.cpp new file mode 100644 index 00000000000..aec93db1876 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.cpp @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "LocalizedStrings.h" +#include + +namespace Microsoft::ReactNative { + +namespace { + +HMODULE GetCurrentModule() noexcept { + static HMODULE s_module = [] { + HMODULE hmod = nullptr; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&GetCurrentModule), + &hmod); + return hmod; + }(); + return s_module; +} + +} // namespace + +std::wstring GetLocalizedString(UINT stringId) noexcept { + LPCWSTR buffer = nullptr; + int length = LoadStringW(GetCurrentModule(), stringId, reinterpret_cast(&buffer), 0); + if (length > 0 && buffer) { + return std::wstring(buffer, static_cast(length)); + } + return {}; +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.h b/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.h new file mode 100644 index 00000000000..2ddb242bae9 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Utils/LocalizedStrings.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +namespace Microsoft::ReactNative { + +std::wstring GetLocalizedString(UINT stringId) noexcept; + +} // namespace Microsoft::ReactNative