diff --git a/Furious/Backends/ExternalCore/Editor.py b/Furious/Backends/ExternalCore/Editor.py index ca509fa..b3db844 100644 --- a/Furious/Backends/ExternalCore/Editor.py +++ b/Furious/Backends/ExternalCore/Editor.py @@ -19,6 +19,7 @@ from __future__ import annotations +from Furious.Frozenlib import Mixins from Furious.Interface import EditorWidgetBinding from Furious.Models import CoreConfiguration, ServerProfile from Furious.Qt import ( @@ -79,7 +80,7 @@ class ExternalCorePathInput(EditorWidgetBinding): self._key = key self._directory = directory - self._title = AppQLabel(_(title), translatable=True) + self._title = AppQLabel(_(title)) self._input = AppQLineEdit() self._input.setPlaceholderText(_(placeholder) if placeholder else '') self._browse = AppQPushButton(_('Browse...')) @@ -159,9 +160,9 @@ class ExternalCoreArgumentsInput(EditorWidgetBinding): """Initialize the single-line command argument editor.""" super().__init__() - self._title = AppQLabel(_('Arguments'), translatable=True) + self._title = AppQLabel(_('Arguments')) self._input = AppQLineEdit() - self._input.setPlaceholderText(_('Space-separated arguments')) + self._input.setPlaceholderText(_('e.g. --config config.json --verbose')) def widgets(self): """Return the form-row label and line editor.""" @@ -203,6 +204,17 @@ class ExternalCoreArgumentsInput(EditorWidgetBinding): ) +class ExternalCoreEnvironmentTextEdit(Mixins.QTranslatable, QPlainTextEdit): + """Provide a runtime-translatable environment placeholder.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def retranslate(self): + """Refresh the translated placeholder without changing its contents.""" + self.setPlaceholderText(_(self.placeholderText())) + + class ExternalCoreEnvironmentInput(EditorWidgetBinding): """Bind KEY=VALUE lines to inherited-environment overrides.""" @@ -210,10 +222,10 @@ class ExternalCoreEnvironmentInput(EditorWidgetBinding): """Initialize the environment override editor.""" super().__init__() - self._title = AppQLabel(_('Environment Variables'), translatable=True) - self._input = QPlainTextEdit() + self._title = AppQLabel(_('Environment Variables')) + self._input = ExternalCoreEnvironmentTextEdit() self._input.setStyleSheet('min-height: 120px; max-height: 136px;') - self._input.setPlaceholderText(_('KEY=VALUE, one per line')) + self._input.setPlaceholderText(_('One per line, e.g. KEY=VALUE')) def widgets(self): """Return the form-row label and environment editor.""" @@ -268,7 +280,7 @@ class ExternalCoreShutdownTimeoutInput(GuiEditorItemTextSpinBox): def __init__(self): """Initialize a one-to-sixty-second timeout input.""" - super().__init__(title=_('Shutdown Timeout (seconds)'), translatable=True) + super().__init__(title=_('Shutdown Timeout (seconds)')) self.setRange(1, 60) @@ -297,7 +309,7 @@ class ExternalCoreApplicationTun2socksInput(GuiEditorItemTextSwitch): def __init__(self): """Initialize the application tun2socks opt-in control.""" - super().__init__(title=_('Use Application Tun2socks'), translatable=True) + super().__init__(title=_('Use Application Tun2socks')) def connectToggled(self, receiver, methodName: str): """Connect a field-state callback without retaining its binding.""" @@ -319,9 +331,9 @@ class ExternalCoreTunRemoteAddressInput(EditorWidgetBinding): """Initialize a hostname-or-IP input independent of process paths.""" super().__init__() - self._title = AppQLabel(_('TUN Remote Address'), translatable=True) + self._title = AppQLabel(_('TUN Remote Address')) self._input = AppQLineEdit() - self._input.setPlaceholderText(_('Hostname, IPv4, or IPv6 address')) + self._input.setPlaceholderText(_('Remote server hostname or IP address')) def widgets(self): """Return the form-row label and remote-address editor.""" @@ -368,7 +380,7 @@ class ExternalCoreConfigurationGroup(GuiEditorWidgetQGroupBox): _('Working Directory'), 'workingDirectory', directory=True, - placeholder=_('Defaults to executable folder'), + placeholder=_('Leave empty to use executable directory'), ), self._argumentsInput, self._environmentInput, diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index 8490ab4..77614b2 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -2797,14 +2797,6 @@ TRANSLATION = { "ZH": "TUN远程地址", "isReviewed": "True" }, - "Hostname, IPv4, or IPv6 address": { - "source": [ - "Furious.Backends.ExternalCore.Editor" - ], - "RU": "Имя узла, адрес IPv4 или IPv6", - "ZH": "主机名、IPv4或IPv6地址", - "isReviewed": "True" - }, "Arguments": { "source": [ "Furious.Backends.ExternalCore.Editor" @@ -2821,14 +2813,6 @@ TRANSLATION = { "ZH": "环境变量", "isReviewed": "True" }, - "KEY=VALUE, one per line": { - "source": [ - "Furious.Backends.ExternalCore.Editor" - ], - "RU": "KEY=VALUE, по одной на строку", - "ZH": "KEY=VALUE,每行一个", - "isReviewed": "True" - }, "Shutdown Timeout (seconds)": { "source": [ "Furious.Backends.ExternalCore.Editor" @@ -2837,22 +2821,6 @@ TRANSLATION = { "ZH": "关闭超时(秒)", "isReviewed": "True" }, - "Space-separated arguments": { - "source": [ - "Furious.Backends.ExternalCore.Editor" - ], - "RU": "Аргументы через пробел", - "ZH": "参数以空格分隔", - "isReviewed": "True" - }, - "Defaults to executable folder": { - "source": [ - "Furious.Backends.ExternalCore.Editor" - ], - "RU": "Папка исполняемого файла", - "ZH": "默认使用可执行文件目录", - "isReviewed": "True" - }, "Arguments contain invalid quoting.": { "source": [ "Furious.Backends.ExternalCore.Editor" @@ -3228,5 +3196,37 @@ TRANSLATION = { "RU": "Например: curl", "ZH": "例如:curl", "isReviewed": "True" + }, + "e.g. --config config.json --verbose": { + "source": [ + "Furious.Backends.ExternalCore.Editor" + ], + "RU": "Например: --config config.json --verbose", + "ZH": "例如:--config config.json --verbose", + "isReviewed": "True" + }, + "One per line, e.g. KEY=VALUE": { + "source": [ + "Furious.Backends.ExternalCore.Editor" + ], + "RU": "По одной на строку, например KEY=VALUE", + "ZH": "每行一个,例如 KEY=VALUE", + "isReviewed": "True" + }, + "Remote server hostname or IP address": { + "source": [ + "Furious.Backends.ExternalCore.Editor" + ], + "RU": "Имя узла или IP-адрес удалённого сервера", + "ZH": "远程服务器的主机名或IP地址", + "isReviewed": "True" + }, + "Leave empty to use executable directory": { + "source": [ + "Furious.Backends.ExternalCore.Editor" + ], + "RU": "Оставьте пустым для каталога исполняемого файла", + "ZH": "留空则使用可执行文件目录", + "isReviewed": "True" } } diff --git a/tests/test_ui_behavior.py b/tests/test_ui_behavior.py index af99195..a160e0f 100644 --- a/tests/test_ui_behavior.py +++ b/tests/test_ui_behavior.py @@ -1050,6 +1050,77 @@ class EditorMappingTest(unittest.TestCase): editor.close() + def testExternalCorePlaceholdersRetranslateWithoutChangingValues(self): + """Translate field guidance without persisting it as configuration.""" + configuration = ConfigExternalCore(copy.deepcopy(BLANK_CONFIG_EXTERNAL_CORE)) + configuration.update( + { + 'arguments': ['--config', 'fixture.json', '--verbose'], + 'environment': {'TOKEN': 'fixture'}, + 'useApplicationTun2socks': True, + 'tunRemoteAddress': 'server.example.com', + } + ) + profile = ServerProfile.fromConfiguration( + configuration, + ProfileMetadata(displayName='Fixture core'), + ) + + with isolatedSettings(): + AppSettings.set('Language', 'EN') + editor = ExternalCoreEditor() + editor.factoryToInput(profile) + original = copy.deepcopy(profile.connection) + basicGroup = editor.groupBoxSequence()[0] + workingDirectoryInput = basicGroup._containers[2]._input + placeholderInputs = ( + ( + workingDirectoryInput, + 'Leave empty to use executable directory', + ), + ( + editor._argumentsInput._input, + 'e.g. --config config.json --verbose', + ), + ( + editor._environmentInput._input, + 'One per line, e.g. KEY=VALUE', + ), + ( + editor._tunRemoteAddressInput._input, + 'Remote server hostname or IP address', + ), + ) + + for widget, sourceText in placeholderInputs: + self.assertEqual(widget.placeholderText(), sourceText) + + for language in ('ZH', 'RU', 'EN'): + AppSettings.set('Language', language) + Mixins.QTranslatable.retranslateAll() + + for widget, sourceText in placeholderInputs: + self.assertEqual(widget.placeholderText(), _(sourceText, language)) + + self.assertEqual( + editor._argumentsInput.values(), + ['--config', 'fixture.json', '--verbose'], + ) + self.assertEqual( + editor._environmentInput.values(), + {'TOKEN': 'fixture'}, + ) + self.assertEqual( + editor._tunRemoteAddressInput.text(), + 'server.example.com', + ) + + self.assertFalse(editor.inputToFactory(profile)) + self.assertEqual(profile.connection, original) + self.assertEqual(workingDirectoryInput.text(), '') + + editor.close() + def testExternalCoreSectionUsesCanonicalEditorInset(self): """Use canonical editor grids and insets for both External Core groups.""" editor = ExternalCoreEditor()