fix: match provider picker to action popup
This commit is contained in:
@@ -132,6 +132,8 @@ Notes autosave locally as RTFD packages, including embedded images, under `~/Lib
|
|||||||
|
|
||||||
Search for **Conjure Translate**, identified by its centered 🌏 icon, and open it. Choose a provider in the pill-shaped header selector, enter text in the search bar, then press Return or click **Translate**. Typing alone never sends text. When the translation is available, the action changes to **Copy**: press Return or click it to copy the full translation and close Conjure. Command-C still copies selected output. Editing the input restores **Translate**.
|
Search for **Conjure Translate**, identified by its centered 🌏 icon, and open it. Choose a provider in the pill-shaped header selector, enter text in the search bar, then press Return or click **Translate**. Typing alone never sends text. When the translation is available, the action changes to **Copy**: press Return or click it to copy the full translation and close Conjure. Command-C still copies selected output. Editing the input restores **Translate**.
|
||||||
|
|
||||||
|
The provider selector opens a searchable action-style popup below the header, with a checkmark beside the current provider. Use Up/Down and Return or click a provider to select it; Back or clicking outside closes the popup. Changing providers clears the previous result without submitting a request.
|
||||||
|
|
||||||
The footer shows the current language pair (initially **English → Japanese**). Click it or open **Actions** with Command-K to change the source language, change the target language, or swap them. Language choices are saved locally and apply to both providers. Changing languages clears the previous result without submitting another request. The input hint and output accessibility label follow the selected pair. Back closes Actions first, then returns to the launcher and restores your search; Escape or Command-W dismisses the entire window.
|
The footer shows the current language pair (initially **English → Japanese**). Click it or open **Actions** with Command-K to change the source language, change the target language, or swap them. Language choices are saved locally and apply to both providers. Changing languages clears the previous result without submitting another request. The input hint and output accessibility label follow the selected pair. Back closes Actions first, then returns to the launcher and restores your search; Escape or Command-W dismisses the entire window.
|
||||||
|
|
||||||
Editing the input, changing languages or providers, saving or forgetting the key in Settings, or leaving Translate cancels the active request; cancelled results are not shown. After a request fails, submit again to retry.
|
Editing the input, changing languages or providers, saving or forgetting the key in Settings, or leaving Translate cancels the active request; cancelled results are not shown. After a request fails, submit again to retry.
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text),
|
ActionEntry(title: "Copy Emoji", symbol: "doc.on.doc", color: SunsetPalette.text),
|
||||||
ActionEntry(title: "Change source language…", symbol: "character.bubble", color: SunsetPalette.text),
|
ActionEntry(title: "Change source language…", symbol: "character.bubble", color: SunsetPalette.text),
|
||||||
ActionEntry(title: "Change target language…", symbol: "character.bubble", color: SunsetPalette.text),
|
ActionEntry(title: "Change target language…", symbol: "character.bubble", color: SunsetPalette.text),
|
||||||
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: SunsetPalette.text)
|
ActionEntry(title: "Swap languages", symbol: "arrow.left.arrow.right", color: SunsetPalette.text),
|
||||||
|
ActionEntry(title: "MyMemory (no key)", symbol: "checkmark", color: SunsetPalette.text),
|
||||||
|
ActionEntry(title: "OpenAI · gpt-6-luna", symbol: "checkmark", color: SunsetPalette.text)
|
||||||
]
|
]
|
||||||
private var visibleEntries: [Int] = []
|
private var visibleEntries: [Int] = []
|
||||||
private var availableEntries = Array(0..<6)
|
private var availableEntries = Array(0..<6)
|
||||||
@@ -41,6 +43,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
var changeSourceLanguage: (() -> Void)?
|
var changeSourceLanguage: (() -> Void)?
|
||||||
var changeTargetLanguage: (() -> Void)?
|
var changeTargetLanguage: (() -> Void)?
|
||||||
var swapLanguages: (() -> Void)?
|
var swapLanguages: (() -> Void)?
|
||||||
|
var selectProvider: ((Bool) -> Void)?
|
||||||
|
private var currentProviderEntry: Int?
|
||||||
var closed: (() -> Void)?
|
var closed: (() -> Void)?
|
||||||
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
var isShown: Bool { viewIfLoaded?.superview != nil && !isClosing }
|
||||||
|
|
||||||
@@ -143,9 +147,22 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
searchAccessibilityLabel: "Search translation actions")
|
searchAccessibilityLabel: "Search translation actions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showProviders(selectedOpenAI: Bool, relativeTo anchor: NSView) {
|
||||||
|
currentProviderEntry = selectedOpenAI ? 11 : 10
|
||||||
|
show(title: "Translation provider", relativeTo: anchor, available: [10, 11],
|
||||||
|
searchAccessibilityLabel: "Search translation providers", belowAnchor: true)
|
||||||
|
if isShown {
|
||||||
|
selectedEntry = currentProviderEntry
|
||||||
|
updateSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func show(title: String, relativeTo anchor: NSView, available: [Int], emoji: Bool = false,
|
private func show(title: String, relativeTo anchor: NSView, available: [Int], emoji: Bool = false,
|
||||||
searchAccessibilityLabel: String = "Search application actions") {
|
searchAccessibilityLabel: String = "Search application actions", belowAnchor: Bool = false) {
|
||||||
if isShown { close(); return }
|
if viewIfLoaded?.superview != nil {
|
||||||
|
if isShown, availableEntries == available { close(); return }
|
||||||
|
close(animated: false)
|
||||||
|
}
|
||||||
guard let window = anchor.window, let host = window.contentView else { return }
|
guard let window = anchor.window, let host = window.contentView else { return }
|
||||||
let pill = anchor.superview ?? anchor
|
let pill = anchor.superview ?? anchor
|
||||||
_ = view
|
_ = view
|
||||||
@@ -163,6 +180,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
])
|
])
|
||||||
favorite.button.setAccessibilityLabel(favoriteTitle)
|
favorite.button.setAccessibilityLabel(favoriteTitle)
|
||||||
favorite.icon.image = NSImage(systemSymbolName: isFavorite ? "star.fill" : "star", accessibilityDescription: nil)
|
favorite.icon.image = NSImage(systemSymbolName: isFavorite ? "star.fill" : "star", accessibilityDescription: nil)
|
||||||
|
for index in [10, 11] {
|
||||||
|
entries[index].icon.isHidden = currentProviderEntry != index
|
||||||
|
}
|
||||||
|
search.placeholderAttributedString = NSAttributedString(
|
||||||
|
string: belowAnchor ? "Search for providers…" : "Search for actions…",
|
||||||
|
attributes: [.foregroundColor: SunsetPalette.secondaryText,
|
||||||
|
.font: NSFont.systemFont(ofSize: LauncherRowView.textSize)])
|
||||||
search.stringValue = ""
|
search.stringValue = ""
|
||||||
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
||||||
transitionGeneration += 1
|
transitionGeneration += 1
|
||||||
@@ -182,16 +206,18 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
])
|
])
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
|
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
|
||||||
view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
|
belowAnchor
|
||||||
|
? view.topAnchor.constraint(equalTo: pill.bottomAnchor, constant: 8)
|
||||||
|
: view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
|
||||||
])
|
])
|
||||||
coveredPill = pill
|
coveredPill = belowAnchor ? nil : pill
|
||||||
coveredPillAlpha = pill.alphaValue
|
coveredPillAlpha = pill.alphaValue
|
||||||
}
|
}
|
||||||
pill.setAccessibilityHidden(true)
|
coveredPill?.setAccessibilityHidden(true)
|
||||||
NSAnimationContext.runAnimationGroup { context in
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.16
|
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.16
|
||||||
view.animator().alphaValue = 1
|
view.animator().alphaValue = 1
|
||||||
pill.animator().alphaValue = 0
|
coveredPill?.animator().alphaValue = 0
|
||||||
}
|
}
|
||||||
window.makeFirstResponder(search)
|
window.makeFirstResponder(search)
|
||||||
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||||
@@ -271,7 +297,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
let selected = selectedEntry == index
|
let selected = selectedEntry == index
|
||||||
entries[index].view.isSelected = selected
|
entries[index].view.isSelected = selected
|
||||||
entries[index].returnKey.isHidden = !selected
|
entries[index].returnKey.isHidden = !selected
|
||||||
entries[index].button.setAccessibilityValue(selected ? "Selected" : "Not selected")
|
let current = currentProviderEntry == index ? ", Current provider" : ""
|
||||||
|
entries[index].button.setAccessibilityValue((selected ? "Selected" : "Not selected") + current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +337,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
case 7: changeSourceLanguage?()
|
case 7: changeSourceLanguage?()
|
||||||
case 8: changeTargetLanguage?()
|
case 8: changeTargetLanguage?()
|
||||||
case 9: swapLanguages?()
|
case 9: swapLanguages?()
|
||||||
|
case 10: selectProvider?(false)
|
||||||
|
case 11: selectProvider?(true)
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Security
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TranslationController: NSViewController {
|
final class TranslationController: NSViewController {
|
||||||
let providerPicker = NSPopUpButton()
|
private let providerButton = NSButton(title: "", target: nil, action: nil)
|
||||||
let providerControl = NSView()
|
let providerControl = NSView()
|
||||||
private let languageButton = NSButton(title: "", target: nil, action: nil)
|
private let languageButton = NSButton(title: "", target: nil, action: nil)
|
||||||
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
|
private let actionsButton = NSButton(title: "Actions", target: nil, action: nil)
|
||||||
@@ -21,7 +21,7 @@ final class TranslationController: NSViewController {
|
|||||||
Locale(identifier: "en").localizedString(forLanguageCode: code) ?? code
|
Locale(identifier: "en").localizedString(forLanguageCode: code) ?? code
|
||||||
}
|
}
|
||||||
private func languageName(_ code: String) -> String { Self.languageName(code) }
|
private func languageName(_ code: String) -> String { Self.languageName(code) }
|
||||||
private var usesMyMemory: Bool { providerPicker.indexOfSelectedItem == 0 }
|
private var usesMyMemory: Bool
|
||||||
private let output = NSTextView()
|
private let output = NSTextView()
|
||||||
private let settingsButton = NSButton(title: "Open Translate Settings", target: nil, action: nil)
|
private let settingsButton = NSButton(title: "Open Translate Settings", target: nil, action: nil)
|
||||||
private let status = NSTextField(wrappingLabelWithString: "")
|
private let status = NSTextField(wrappingLabelWithString: "")
|
||||||
@@ -52,6 +52,7 @@ final class TranslationController: NSViewController {
|
|||||||
self.myMemory = myMemory
|
self.myMemory = myMemory
|
||||||
self.openAI = openAI
|
self.openAI = openAI
|
||||||
self.pasteboard = pasteboard
|
self.pasteboard = pasteboard
|
||||||
|
usesMyMemory = defaults.string(forKey: "translationProvider") != "openai"
|
||||||
sourceLanguage = defaults.string(forKey: "translationSourceLanguage")
|
sourceLanguage = defaults.string(forKey: "translationSourceLanguage")
|
||||||
.flatMap { Self.languages.contains($0) ? $0 : nil } ?? "en"
|
.flatMap { Self.languages.contains($0) ? $0 : nil } ?? "en"
|
||||||
targetLanguage = defaults.string(forKey: "translationTargetLanguage")
|
targetLanguage = defaults.string(forKey: "translationTargetLanguage")
|
||||||
@@ -63,30 +64,28 @@ final class TranslationController: NSViewController {
|
|||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = NSView()
|
view = NSView()
|
||||||
providerPicker.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
|
providerButton.title = usesMyMemory ? "MyMemory (no key)" : "OpenAI · gpt-6-luna"
|
||||||
providerPicker.selectItem(at: defaults.string(forKey: "translationProvider") == "openai" ? 1 : 0)
|
providerButton.setAccessibilityLabel("Translation provider")
|
||||||
providerPicker.setAccessibilityLabel("Translation provider")
|
providerButton.target = self
|
||||||
providerPicker.target = self
|
providerButton.action = #selector(showProviders)
|
||||||
providerPicker.action = #selector(providerChanged)
|
providerButton.isBordered = false
|
||||||
providerPicker.isBordered = false
|
providerButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
(providerPicker.cell as? NSPopUpButtonCell)?.arrowPosition = .noArrow
|
providerButton.contentTintColor = SunsetPalette.text
|
||||||
providerPicker.font = .systemFont(ofSize: 12, weight: .medium)
|
|
||||||
providerPicker.contentTintColor = SunsetPalette.text
|
|
||||||
let chevron = NSButton(image: NSImage(systemSymbolName: "chevron.down", accessibilityDescription: nil)!,
|
let chevron = NSButton(image: NSImage(systemSymbolName: "chevron.down", accessibilityDescription: nil)!,
|
||||||
target: providerPicker, action: #selector(NSControl.performClick(_:)))
|
target: self, action: #selector(showProviders))
|
||||||
chevron.isBordered = false
|
chevron.isBordered = false
|
||||||
chevron.contentTintColor = SunsetPalette.secondaryText
|
chevron.contentTintColor = SunsetPalette.secondaryText
|
||||||
chevron.setAccessibilityElement(false)
|
chevron.setAccessibilityElement(false)
|
||||||
stylePill(providerControl)
|
stylePill(providerControl)
|
||||||
for control in [providerPicker, chevron] {
|
for control in [providerButton, chevron] {
|
||||||
control.translatesAutoresizingMaskIntoConstraints = false
|
control.translatesAutoresizingMaskIntoConstraints = false
|
||||||
providerControl.addSubview(control)
|
providerControl.addSubview(control)
|
||||||
}
|
}
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
providerControl.heightAnchor.constraint(equalToConstant: 36),
|
providerControl.heightAnchor.constraint(equalToConstant: 36),
|
||||||
providerPicker.leadingAnchor.constraint(equalTo: providerControl.leadingAnchor, constant: 14),
|
providerButton.leadingAnchor.constraint(equalTo: providerControl.leadingAnchor, constant: 14),
|
||||||
providerPicker.centerYAnchor.constraint(equalTo: providerControl.centerYAnchor),
|
providerButton.centerYAnchor.constraint(equalTo: providerControl.centerYAnchor),
|
||||||
chevron.leadingAnchor.constraint(equalTo: providerPicker.trailingAnchor, constant: 8),
|
chevron.leadingAnchor.constraint(equalTo: providerButton.trailingAnchor, constant: 8),
|
||||||
chevron.centerYAnchor.constraint(equalTo: providerControl.centerYAnchor),
|
chevron.centerYAnchor.constraint(equalTo: providerControl.centerYAnchor),
|
||||||
chevron.trailingAnchor.constraint(equalTo: providerControl.trailingAnchor, constant: -12),
|
chevron.trailingAnchor.constraint(equalTo: providerControl.trailingAnchor, constant: -12),
|
||||||
chevron.widthAnchor.constraint(equalToConstant: 10),
|
chevron.widthAnchor.constraint(equalToConstant: 10),
|
||||||
@@ -112,6 +111,7 @@ final class TranslationController: NSViewController {
|
|||||||
actions.changeSourceLanguage = { [weak self] in self?.chooseLanguage(source: true) }
|
actions.changeSourceLanguage = { [weak self] in self?.chooseLanguage(source: true) }
|
||||||
actions.changeTargetLanguage = { [weak self] in self?.chooseLanguage(source: false) }
|
actions.changeTargetLanguage = { [weak self] in self?.chooseLanguage(source: false) }
|
||||||
actions.swapLanguages = { [weak self] in self?.swapLanguages() }
|
actions.swapLanguages = { [weak self] in self?.swapLanguages() }
|
||||||
|
actions.selectProvider = { [weak self] in self?.selectProvider(openAI: $0) }
|
||||||
settingsButton.target = self
|
settingsButton.target = self
|
||||||
settingsButton.action = #selector(openSettings)
|
settingsButton.action = #selector(openSettings)
|
||||||
settingsButton.isHidden = true
|
settingsButton.isHidden = true
|
||||||
@@ -196,6 +196,11 @@ final class TranslationController: NSViewController {
|
|||||||
pill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
pill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func showProviders() {
|
||||||
|
actions.showProviders(selectedOpenAI: !usesMyMemory, relativeTo: providerButton)
|
||||||
|
translateButton.keyEquivalent = actions.isShown ? "" : "\r"
|
||||||
|
}
|
||||||
|
|
||||||
@objc func showActions() {
|
@objc func showActions() {
|
||||||
actions.showTranslation(relativeTo: actionsButton)
|
actions.showTranslation(relativeTo: actionsButton)
|
||||||
translateButton.keyEquivalent = actions.isShown ? "" : "\r"
|
translateButton.keyEquivalent = actions.isShown ? "" : "\r"
|
||||||
@@ -267,8 +272,12 @@ final class TranslationController: NSViewController {
|
|||||||
setStatus("")
|
setStatus("")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func providerChanged() {
|
func selectProvider(openAI: Bool) {
|
||||||
defaults.set(usesMyMemory ? "mymemory" : "openai", forKey: "translationProvider")
|
let newUsesMyMemory = !openAI
|
||||||
|
guard newUsesMyMemory != usesMyMemory else { return }
|
||||||
|
usesMyMemory = newUsesMyMemory
|
||||||
|
providerButton.title = usesMyMemory ? "MyMemory (no key)" : "OpenAI · gpt-6-luna"
|
||||||
|
defaults.set(openAI ? "openai" : "mymemory", forKey: "translationProvider")
|
||||||
inputChanged()
|
inputChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,3 +29,35 @@ import Testing
|
|||||||
actions.close(animated: false)
|
actions.close(animated: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func providerPickerOpensBelowAnchorAndKeepsCurrentSelection() throws {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 480),
|
||||||
|
styleMask: .borderless, backing: .buffered, defer: false)
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
let host = try #require(window.contentView)
|
||||||
|
let pill = NSView(frame: NSRect(x: 500, y: 420, width: 230, height: 36))
|
||||||
|
let anchor = NSButton(frame: pill.bounds)
|
||||||
|
pill.addSubview(anchor)
|
||||||
|
host.addSubview(pill)
|
||||||
|
let popup = ApplicationActionsController()
|
||||||
|
defer { popup.close(animated: false); window.orderOut(nil) }
|
||||||
|
var selections: [Bool] = []
|
||||||
|
popup.selectProvider = { selections.append($0) }
|
||||||
|
popup.showProviders(selectedOpenAI: true, relativeTo: anchor)
|
||||||
|
host.layoutSubtreeIfNeeded()
|
||||||
|
let rect = popup.view.convert(popup.view.bounds, to: host)
|
||||||
|
#expect(rect.maxY < pill.frame.minY)
|
||||||
|
#expect(rect.maxX == pill.frame.maxX)
|
||||||
|
#expect(pill.alphaValue == 1)
|
||||||
|
_ = popup.handle(#selector(NSResponder.insertNewline(_:)))
|
||||||
|
#expect(selections == [true])
|
||||||
|
// Reopen during the close animation, then switch modes without retaining the old anchor.
|
||||||
|
popup.showTranslation(relativeTo: anchor)
|
||||||
|
popup.showProviders(selectedOpenAI: false, relativeTo: anchor)
|
||||||
|
host.layoutSubtreeIfNeeded()
|
||||||
|
#expect(popup.view.convert(popup.view.bounds, to: host).maxY < pill.frame.minY)
|
||||||
|
popup.moveSelection(by: 1)
|
||||||
|
_ = popup.handle(#selector(NSResponder.insertNewline(_:)))
|
||||||
|
#expect(selections == [true, true])
|
||||||
|
}
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ func translationExecutionRejectsOldCompletionsWithoutClearingNewRequests() async
|
|||||||
tool.input = { input }
|
tool.input = { input }
|
||||||
let views = descendants(tool.view)
|
let views = descendants(tool.view)
|
||||||
let output = try #require(views.compactMap { $0 as? NSTextView }.first)
|
let output = try #require(views.compactMap { $0 as? NSTextView }.first)
|
||||||
let provider = tool.providerPicker
|
|
||||||
let button = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("performPrimaryAction") })
|
let button = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("performPrimaryAction") })
|
||||||
tool.translate()
|
tool.translate()
|
||||||
try await waitForTranslation { requests.pending.count == 1 }
|
try await waitForTranslation { requests.pending.count == 1 }
|
||||||
@@ -200,8 +199,7 @@ func translationExecutionRejectsOldCompletionsWithoutClearingNewRequests() async
|
|||||||
tool.translate()
|
tool.translate()
|
||||||
#expect(requests.inputs.count == 2)
|
#expect(requests.inputs.count == 2)
|
||||||
|
|
||||||
provider.selectItem(at: 1)
|
tool.selectProvider(openAI: true)
|
||||||
_ = tool.perform(provider.action, with: provider)
|
|
||||||
#expect(defaults.string(forKey: "translationProvider") == "openai")
|
#expect(defaults.string(forKey: "translationProvider") == "openai")
|
||||||
tool.translate()
|
tool.translate()
|
||||||
try await waitForTranslation { requests.pending.count == 3 }
|
try await waitForTranslation { requests.pending.count == 3 }
|
||||||
|
|||||||
Reference in New Issue
Block a user