diff --git a/PartnerCtl/pom.xml b/PartnerCtl/pom.xml index 03aac9a0..e2d8369d 100644 --- a/PartnerCtl/pom.xml +++ b/PartnerCtl/pom.xml @@ -115,6 +115,7 @@ --no-fallback -H:+ReportExceptionStackTraces --initialize-at-build-time=kotlin.DeprecationLevel + -H:IncludeResourceBundles=i18n.messages diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/InitCommand.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/InitCommand.kt index 87fa81e9..0da6b09c 100644 --- a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/InitCommand.kt +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/InitCommand.kt @@ -9,6 +9,7 @@ import work.slhaf.partner.ctl.commands.init.buildFromSource import work.slhaf.partner.ctl.commands.init.configureExternalGateway import work.slhaf.partner.ctl.commands.init.configureOpenAiCompatible import work.slhaf.partner.ctl.commands.init.configureWebSocketGateway +import work.slhaf.partner.ctl.i18n.I18n.text import work.slhaf.partner.ctl.support.CommandInterrupted import work.slhaf.partner.ctl.support.inheritCommand import work.slhaf.partner.ctl.support.loadAvailableGateway @@ -52,7 +53,7 @@ class InitCommand : Runnable { } private fun initHome(prompt: Prompt) { - prompt.section("Initialize Partner Home") + prompt.section(text("init.home.section")) home = choosePartnerHome(prompt) @@ -60,7 +61,7 @@ class InitCommand : Runnable { Files.createDirectories(home.resolve("resource")) Files.createDirectories(home.resolve("config")) - prompt.success("Partner Home initialized at $home") + prompt.success(text("init.home.success", home)) } private fun choosePartnerHome(prompt: Prompt): Path { @@ -68,7 +69,7 @@ class InitCommand : Runnable { while (true) { val selectedHome = prompt.askPath( - label = "Partner Home", + label = text("init.home.label"), defaultValue = defaultHome, required = true, directoryOnly = true, @@ -78,29 +79,29 @@ class InitCommand : Runnable { return selectedHome } - prompt.warn("Partner Home already contains files: $selectedHome") + prompt.warn(text("init.home.duplicate.warning", selectedHome)) when (prompt.select( - label = "Partner Home already contains files. Choose how to continue", + label = text("init.home.duplicate.choice.label"), choices = listOf( Choice( - "Use another Partner Home", + text("init.home.duplicate.choice.another"), HomeDuplicateChoice.ANOTHER, - "Choose a different directory", + text("init.home.duplicate.choice.another.description"), ), Choice( - "Overwrite current Partner Home", + text("init.home.duplicate.choice.overwrite"), HomeDuplicateChoice.OVERWRITE, - "Delete existing contents and continue", + text("init.home.duplicate.choice.overwrite.description"), ), - Choice("Cancel init", HomeDuplicateChoice.EXIT), + Choice(text("init.home.duplicate.choice.exit"), HomeDuplicateChoice.EXIT), ), defaultIndex = 0, )) { HomeDuplicateChoice.ANOTHER -> continue HomeDuplicateChoice.OVERWRITE -> { validateSafeHomeOverwrite(selectedHome) - if (!prompt.confirm("Delete all files under $selectedHome?", false)) { + if (!prompt.confirm(text("init.home.duplicate.confirmDelete", selectedHome), false)) { continue } clearHomeDirectory(selectedHome) @@ -134,7 +135,7 @@ class InitCommand : Runnable { private fun clearHomeDirectory(path: Path) { if (!Files.exists(path)) return if (!Files.isDirectory(path)) { - throw CommandInterrupted("Partner Home is not a directory: $path") + throw CommandInterrupted(text("init.home.notDirectory", path)) } Files.walk(path).use { stream -> @@ -150,25 +151,25 @@ class InitCommand : Runnable { val userHome = Paths.get(System.getProperty("user.home")).toAbsolutePath().normalize() if (normalized == normalized.root) { - throw CommandInterrupted("Refuse to overwrite filesystem root: $normalized") + throw CommandInterrupted(text("init.home.overwrite.refuseRoot", normalized)) } if (normalized == userHome) { - throw CommandInterrupted("Refuse to overwrite user home directory: $normalized") + throw CommandInterrupted(text("init.home.overwrite.refuseUserHome", normalized)) } if (normalized.nameCount < 2) { - throw CommandInterrupted("Refuse to overwrite suspiciously broad directory: $normalized") + throw CommandInterrupted(text("init.home.overwrite.refuseBroadDirectory", normalized)) } } private fun installPartner(prompt: Prompt) { - prompt.section("Install Partner") + prompt.section(text("init.install.section")) val installChoice = prompt.select( - label = "Choose a installation method", - choices = listOf(Choice("Build Partner from source", InstallChoice.BUILD_FROM_SOURCE)) + label = text("init.install.method.label"), + choices = listOf(Choice(text("init.install.method.buildFromSource"), InstallChoice.BUILD_FROM_SOURCE)) ) when (installChoice) { @@ -178,12 +179,12 @@ class InitCommand : Runnable { } private fun configureGateway(prompt: Prompt) { - prompt.section("Configure Gateway") + prompt.section(text("init.gateway.section")) val providedGateways = loadAvailableGateway() val selectedGateways = prompt.multiSelect( - label = "Select gateway", - choices = listOf(Choice("WebSocket Gateway", "websocket_channel")) + + label = text("init.gateway.select.label"), + choices = listOf(Choice(text("init.gateway.websocket.choice"), "websocket_channel")) + providedGateways.map { Choice(it.name, it.id) } @@ -198,29 +199,29 @@ class InitCommand : Runnable { if (manifest != null) { return@map configureExternalGateway(home, prompt, manifest) } else { - prompt.warn("Could not find gateway with id $gateway") + prompt.warn(text("init.gateway.warn.notFound", gateway)) return@map null } } } catch (_: PromptCancelledException) { - prompt.warn("Gateway: $gateway configuration skipped") + prompt.warn(text("init.gateway.warn.skipped", gateway)) return@map null } }.filterNotNull() val defaultChannel = if (configuredChannels.isEmpty()) { - prompt.info("Skipped gateway configuration. Partner will use WebSocket as default gateway") + prompt.info(text("init.gateway.info.skippedUseDefault")) return } else if (configuredChannels.size == 1) { configuredChannels.first().channelName } else { prompt.select( - label = "Set default channel", + label = text("init.gateway.defaultChannel.label"), choices = configuredChannels.map { Choice(it.channelName, it.channelName) } ) } - prompt.info("The default channel will be set to $defaultChannel") + prompt.info(text("init.gateway.info.defaultChannel", defaultChannel)) val gatewayConfig = GatewayConfig( defaultChannel = defaultChannel, @@ -236,11 +237,11 @@ class InitCommand : Runnable { val gatewayPath = home.resolve("config").resolve("gateway.json").toAbsolutePath().normalize() Files.writeString(gatewayPath, gatewayStr) - prompt.success("Gateway config written to $gatewayPath") + prompt.success(text("init.gateway.success.configWritten", gatewayPath)) } private fun configureModel(prompt: Prompt) { - prompt.section("Configure Model") + prompt.section(text("init.model.section")) val modelChoices = ModelProviderChoice.entries.map { Choice(it.display, it) } @@ -250,9 +251,9 @@ class InitCommand : Runnable { while (true) { val choice = prompt.select( label = if (!defaultAlreadySet) { - "Choose default model provider type" + text("init.model.provider.default.label") } else { - "Choose model provider type" + text("init.model.provider.label") }, choices = modelChoices ) @@ -264,10 +265,9 @@ class InitCommand : Runnable { break } else { prompt.warn( - "No default model provider configured. Partner may not start normally unless model.json exists " + - "or PARTNER_DEFAULT_BASE_URL, PARTNER_DEFAULT_API_KEY, and PARTNER_DEFAULT_MODEL are provided at runtime." + text("init.model.warn.noDefaultProvider") ) - if (prompt.confirm("Skip model configuration?", false)) { + if (prompt.confirm(text("init.model.confirm.skipConfiguration"), false)) { break } else { null @@ -280,7 +280,7 @@ class InitCommand : Runnable { chosenModelProviders.add(it) if (!defaultAlreadySet) { defaultAlreadySet = true - if (!prompt.confirm("Add additional model provider?", false)) { + if (!prompt.confirm(text("init.model.confirm.addAdditionalProvider"), false)) { break } } @@ -306,15 +306,15 @@ class InitCommand : Runnable { val modelPath = home.resolve("config").resolve("model.json").toAbsolutePath().normalize() Files.writeString(modelPath, json.encodeToString(JsonObject.serializer(), jsonObject)) - prompt.success("Model config written to $modelPath") + prompt.success(text("init.model.success.configWritten", modelPath)) } } private fun finalize(prompt: Prompt) { - prompt.section("Finish") + prompt.section(text("init.finish.section")) - if (!prompt.confirm("Start Partner now?", false)) { - prompt.info("Initialization completed.") + if (!prompt.confirm(text("init.finish.confirm.startNow"), false)) { + prompt.info(text("init.finish.info.completed")) return } @@ -323,7 +323,7 @@ class InitCommand : Runnable { throw CommandInterrupted("Partner runtime jar does not exist: $partnerJar") } - prompt.info("Starting Partner...") + prompt.info(text("init.finish.info.starting")) val exitCode = inheritCommand( command = listOf("java", "-jar", partnerJar.toString()), environment = mapOf("PARTNER_HOME" to home.toString()), diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/init/configure.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/init/configure.kt index 05736ce5..cf048606 100644 --- a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/init/configure.kt +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/commands/init/configure.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.json.* import work.slhaf.partner.ctl.commands.data.GatewayConfig import work.slhaf.partner.ctl.commands.data.OpenAiCompatible import work.slhaf.partner.ctl.commands.data.ProviderConfig +import work.slhaf.partner.ctl.i18n.I18n.text import work.slhaf.partner.ctl.support.* import work.slhaf.partner.ctl.ui.Prompt import java.net.URI @@ -13,26 +14,26 @@ import java.nio.file.Paths import kotlin.io.path.isDirectory fun configureWebSocketGateway(prompt: Prompt): GatewayConfig.ChannelConfig { - prompt.section("Configure Gateway: WebSocket Gateway") + prompt.section(text("configure.gateway.websocket.section")) - val port = prompt.ask("port", "29600") { - val intValue = it.toIntOrNull() ?: return@ask "WebSocket port only accepts int value" - if (intValue !in 1..65565) "WebSocket port should be between 1 and 65565" else null + val port = prompt.ask(text("configure.gateway.websocket.port.label"), "29600") { + val intValue = it.toIntOrNull() ?: return@ask text("configure.gateway.websocket.port.error.int") + if (intValue !in 1..65565) text("configure.gateway.websocket.port.error.range") else null }.toInt() - val heartbeatInterval = prompt.ask("heartbeat interval", "10000") { - it.toLongOrNull() ?: return@ask "Heartbeat interval only accepts long value" + val heartbeatInterval = prompt.ask(text("configure.gateway.websocket.heartbeatInterval.label"), "10000") { + it.toLongOrNull() ?: return@ask text("configure.gateway.websocket.heartbeatInterval.error.long") return@ask null }.toLong() - val hostname = prompt.ask("Listening hostname", "127.0.0.1") { + val hostname = prompt.ask(text("configure.gateway.websocket.hostname.label"), "127.0.0.1") { val host = it.trim() return@ask when { - host.isEmpty() -> "Hostname is required" - host.contains(Regex("\\s")) -> "Hostname must not contain whitespace." - host.contains("://") -> "Do not include protocol. Use hostname only, for example: 127.0.0.1" - host.contains("/") -> "Do not include path. Use hostname only." - looksLikeHostWithPort(host) -> "Do not include port here. Port is configured separately." + host.isEmpty() -> text("configure.gateway.websocket.hostname.error.required") + host.contains(Regex("\\s")) -> text("configure.gateway.websocket.hostname.error.whitespace") + host.contains("://") -> text("configure.gateway.websocket.hostname.error.protocol") + host.contains("/") -> text("configure.gateway.websocket.hostname.error.path") + looksLikeHostWithPort(host) -> text("configure.gateway.websocket.hostname.error.port") else -> null } } @@ -53,21 +54,21 @@ private fun looksLikeHostWithPort(value: String): Boolean { } fun configureExternalGateway(home: Path, prompt: Prompt, manifest: ModuleManifest): GatewayConfig.ChannelConfig? { - prompt.section("Configure Gateway: ${manifest.name}") + prompt.section(text("configure.gateway.external.section", manifest.name)) prompt.details( - title = "Gateway module details", + title = text("configure.gateway.external.details.title"), items = listOf( - "Description" to manifest.description, - "Source" to manifest.source.url, - "Build command" to manifest.source.buildCommand.joinToString(" "), - "Artifact" to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}", - "Install target" to manifest.install.target, - "Config target" to (manifest.config?.target ?: "No config"), + text("configure.gateway.external.details.description") to manifest.description, + text("configure.gateway.external.details.source") to manifest.source.url, + text("configure.gateway.external.details.buildCommand") to manifest.source.buildCommand.joinToString(" "), + text("configure.gateway.external.details.artifact") to "${manifest.source.artifactDirectory}/${manifest.source.artifactPattern}", + text("configure.gateway.external.details.installTarget") to manifest.install.target, + text("configure.gateway.external.details.configTarget") to (manifest.config?.target ?: text("configure.gateway.external.details.noConfig")), ), ) - if (!prompt.confirm("Continue installation?", true)) { + if (!prompt.confirm(text("configure.gateway.external.confirmContinue"), true)) { return null } @@ -143,20 +144,20 @@ private fun validateFieldValue(field: Field, value: String): String? { return when (field.type) { FieldType.STRING -> null - FieldType.INT -> value.toIntOrNull()?.let { null } ?: "${field.label} only accepts int value" - FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: "${field.label} only accepts number value" - FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: "${field.label} only accepts true or false" + FieldType.INT -> value.toIntOrNull()?.let { null } ?: text("configure.field.error.int", field.label) + FieldType.NUMBER -> value.toDoubleOrNull()?.let { null } ?: text("configure.field.error.number", field.label) + FieldType.BOOLEAN -> value.toBooleanStrictOrNull()?.let { null } ?: text("configure.field.error.boolean", field.label) FieldType.RAW_JSON -> runCatching { Json.parseToJsonElement(value) } .exceptionOrNull() - ?.let { "${field.label} only accepts valid JSON" } + ?.let { text("configure.field.error.rawJson", field.label) } } } fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): ProviderConfig { val name = if (defaultAlreadySet) { - prompt.ask("Provider name") { + prompt.ask(text("configure.model.openAiCompatible.providerName.label")) { if (it == "default") { - "Default provider cannot be duplicate" + text("configure.model.openAiCompatible.providerName.error.duplicateDefault") } else { null } @@ -165,12 +166,12 @@ fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): Provi "default" } - val baseUrl = prompt.ask("Base url") { value -> + val baseUrl = prompt.ask(text("configure.model.openAiCompatible.baseUrl.label")) { value -> validateNetworkUrl(value) } - val apikey = prompt.ask("Apikey") - val defaultModel = prompt.ask("Default model") + val apikey = prompt.ask(text("configure.model.openAiCompatible.apiKey.label")) + val defaultModel = prompt.ask(text("configure.model.openAiCompatible.defaultModel.label")) return OpenAiCompatible( name = name, baseUrl = baseUrl, @@ -182,18 +183,18 @@ fun configureOpenAiCompatible(prompt: Prompt, defaultAlreadySet: Boolean): Provi private fun validateNetworkUrl(value: String): String? { val trimmed = value.trim() if (trimmed.isEmpty()) { - return "Base url is required" + return text("configure.model.openAiCompatible.baseUrl.error.required") } val uri = runCatching { URI(trimmed) }.getOrElse { - return "Base url must be a valid URL" + return text("configure.model.openAiCompatible.baseUrl.error.validUrl") } return when { - uri.scheme !in setOf("http", "https") -> "Base url must start with http:// or https://" - uri.host.isNullOrBlank() -> "Base url must include a valid host" - uri.rawUserInfo != null -> "Base url must not include user info" - uri.rawFragment != null -> "Base url must not include fragment" + uri.scheme !in setOf("http", "https") -> text("configure.model.openAiCompatible.baseUrl.error.scheme") + uri.host.isNullOrBlank() -> text("configure.model.openAiCompatible.baseUrl.error.host") + uri.rawUserInfo != null -> text("configure.model.openAiCompatible.baseUrl.error.userInfo") + uri.rawFragment != null -> text("configure.model.openAiCompatible.baseUrl.error.fragment") else -> null } } \ No newline at end of file diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/i18n/I18n.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/i18n/I18n.kt new file mode 100644 index 00000000..ac33e748 --- /dev/null +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/i18n/I18n.kt @@ -0,0 +1,20 @@ +package work.slhaf.partner.ctl.i18n + +import java.text.MessageFormat +import java.util.* + +object I18n { + private const val BUNDLE = "i18n.messages" + + private val locale: Locale by lazy { + System.getenv("PARTNER_LOCALE") + ?.takeIf { it.isNotBlank() } + ?.let { Locale.forLanguageTag(it.replace('_', '-')) } + ?: Locale.getDefault() + } + + fun text(key: String, vararg args: Any?): String { + val pattern = ResourceBundle.getBundle(BUNDLE, locale).getString(key) + return MessageFormat.format(pattern, *args) + } +} diff --git a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/support/source_build.kt b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/support/source_build.kt index 172ae09c..aac6ba8a 100644 --- a/PartnerCtl/src/main/java/work/slhaf/partner/ctl/support/source_build.kt +++ b/PartnerCtl/src/main/java/work/slhaf/partner/ctl/support/source_build.kt @@ -1,5 +1,6 @@ package work.slhaf.partner.ctl.support +import work.slhaf.partner.ctl.i18n.I18n.text import work.slhaf.partner.ctl.ui.Prompt import java.nio.file.Files import java.nio.file.Path @@ -51,30 +52,30 @@ fun buildAndInstallFromSource( val targetPath = home.resolve(spec.installRelativePath) try { - prompt.info("Cloning ${spec.displayName} source from ${spec.repoUrl}") + prompt.info(text("sourceBuild.info.cloning", spec.displayName, spec.repoUrl)) val cloneExitCode = inheritCommand( command = listOf("git", "clone", "--depth", "1", spec.repoUrl, sourceDir.toString()), ) if (cloneExitCode != 0) { - throw CommandInterrupted("Failed to clone ${spec.displayName} source from ${spec.repoUrl}") + throw CommandInterrupted(text("sourceBuild.error.cloneFailed", spec.displayName, spec.repoUrl)) } - prompt.info("Building ${spec.displayName}") + prompt.info(text("sourceBuild.info.building", spec.displayName)) val buildExitCode = inheritCommand( command = spec.buildCommand, workingDirectory = sourceDir, ) if (buildExitCode != 0) { - throw CommandInterrupted("Failed to build ${spec.displayName}.") + throw CommandInterrupted(text("sourceBuild.error.buildFailed", spec.displayName)) } val artifactDir = sourceDir.resolve(spec.artifactDirectory) val artifact = spec.artifactSelector(artifactDir) - ?: throw CommandInterrupted("Could not find built ${spec.displayName} artifact in $artifactDir") + ?: throw CommandInterrupted(text("sourceBuild.error.artifactNotFound", spec.displayName, artifactDir)) Files.createDirectories(targetPath.parent) Files.copy(artifact, targetPath, StandardCopyOption.REPLACE_EXISTING) - prompt.success("${spec.displayName} installed at $targetPath") + prompt.success(text("sourceBuild.success.installed", spec.displayName, targetPath)) } finally { runCatching { tempDir.toFile().deleteRecursively() } } @@ -83,31 +84,31 @@ fun buildAndInstallFromSource( private fun checkTool() { fun buildToolLackedMessage(lack: Map): String { return buildString { - appendLine("Missing required build tools:") + appendLine(text("sourceBuild.tool.missing.header")) appendLine() lack.forEach { (tool, reason) -> - appendLine("$tool:") - appendLine(" $reason") + appendLine(text("sourceBuild.tool.missing.item", tool)) + appendLine(text("sourceBuild.tool.missing.reason", reason)) } appendLine() - appendLine("Install the missing tools, then rerun:") + appendLine(text("sourceBuild.tool.missing.footer")) appendLine(" partnerctl init") }.trimEnd() } val lack = mutableMapOf() if (runCommand(listOf("java", "--version")).exitCode != 0) { - lack["java"] = "Required to run Maven. Command failed: java --version" + lack["java"] = text("sourceBuild.tool.java.reason") } if (runCommand(listOf("javac", "--version")).exitCode != 0) { lack["javac"] = - "Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version" + text("sourceBuild.tool.javac.reason") } if (runCommand(listOf("git", "--version")).exitCode != 0) { - lack["git"] = "Required to clone Partner source. Command failed: git --version" + lack["git"] = text("sourceBuild.tool.git.reason") } if (runCommand(listOf("mvn", "--version")).exitCode != 0) { - lack["mvn"] = "Required to build Partner from source. Command failed: mvn --version" + lack["mvn"] = text("sourceBuild.tool.mvn.reason") } if (lack.isNotEmpty()) { throw CommandInterrupted(buildToolLackedMessage(lack)) diff --git a/PartnerCtl/src/main/resources/i18n/messages.properties b/PartnerCtl/src/main/resources/i18n/messages.properties new file mode 100644 index 00000000..19832ad8 --- /dev/null +++ b/PartnerCtl/src/main/resources/i18n/messages.properties @@ -0,0 +1,91 @@ +init.home.section=Initialize Partner Home +init.home.label=Partner Home +init.home.success=Partner Home initialized at {0} +init.home.duplicate.warning=Partner Home already contains files: {0} +init.home.duplicate.choice.label=Partner Home already contains files. Choose how to continue +init.home.duplicate.choice.another=Use another Partner Home +init.home.duplicate.choice.another.description=Choose a different directory +init.home.duplicate.choice.overwrite=Overwrite current Partner Home +init.home.duplicate.choice.overwrite.description=Delete existing contents and continue +init.home.duplicate.choice.exit=Cancel init +init.home.duplicate.confirmDelete=Delete all files under {0}? +init.home.notDirectory=Partner Home is not a directory: {0} +init.home.overwrite.refuseRoot=Refuse to overwrite filesystem root: {0} +init.home.overwrite.refuseUserHome=Refuse to overwrite user home directory: {0} +init.home.overwrite.refuseBroadDirectory=Refuse to overwrite suspiciously broad directory: {0} +init.install.section=Install Partner +init.install.method.label=Choose an installation method +init.install.method.buildFromSource=Build Partner from source +init.gateway.section=Configure Gateway +init.gateway.select.label=Select gateway +init.gateway.websocket.choice=WebSocket Gateway +init.gateway.warn.notFound=Could not find gateway with id {0} +init.gateway.warn.skipped=Gateway: {0} configuration skipped +init.gateway.info.skippedUseDefault=Skipped gateway configuration. Partner will use WebSocket as default gateway +init.gateway.defaultChannel.label=Set default channel +init.gateway.info.defaultChannel=The default channel will be set to {0} +init.gateway.success.configWritten=Gateway config written to {0} +init.model.section=Configure Model +init.model.provider.default.label=Choose default model provider type +init.model.provider.label=Choose model provider type +init.model.warn.noDefaultProvider=No default model provider configured. Partner may not start normally unless model.json exists or PARTNER_DEFAULT_BASE_URL, PARTNER_DEFAULT_API_KEY, and PARTNER_DEFAULT_MODEL are provided at runtime. +init.model.confirm.skipConfiguration=Skip model configuration? +init.model.confirm.addAdditionalProvider=Add additional model provider? +init.model.success.configWritten=Model config written to {0} +init.finish.section=Finish +init.finish.confirm.startNow=Start Partner now? +init.finish.info.completed=Initialization completed. +init.finish.info.starting=Starting Partner... + +configure.gateway.websocket.section=Configure Gateway: WebSocket Gateway +configure.gateway.websocket.port.label=port +configure.gateway.websocket.port.error.int=WebSocket port only accepts int value +configure.gateway.websocket.port.error.range=WebSocket port should be between 1 and 65565 +configure.gateway.websocket.heartbeatInterval.label=heartbeat interval +configure.gateway.websocket.heartbeatInterval.error.long=Heartbeat interval only accepts long value +configure.gateway.websocket.hostname.label=Listening hostname +configure.gateway.websocket.hostname.error.required=Hostname is required +configure.gateway.websocket.hostname.error.whitespace=Hostname must not contain whitespace. +configure.gateway.websocket.hostname.error.protocol=Do not include protocol. Use hostname only, for example: 127.0.0.1 +configure.gateway.websocket.hostname.error.path=Do not include path. Use hostname only. +configure.gateway.websocket.hostname.error.port=Do not include port here. Port is configured separately. +configure.gateway.external.section=Configure Gateway: {0} +configure.gateway.external.details.title=Gateway module details +configure.gateway.external.details.description=Description +configure.gateway.external.details.source=Source +configure.gateway.external.details.buildCommand=Build command +configure.gateway.external.details.artifact=Artifact +configure.gateway.external.details.installTarget=Install target +configure.gateway.external.details.configTarget=Config target +configure.gateway.external.details.noConfig=No config +configure.gateway.external.confirmContinue=Continue installation? +configure.field.error.int={0} only accepts int value +configure.field.error.number={0} only accepts number value +configure.field.error.boolean={0} only accepts true or false +configure.field.error.rawJson={0} only accepts valid JSON +configure.model.openAiCompatible.providerName.label=Provider name +configure.model.openAiCompatible.providerName.error.duplicateDefault=Default provider cannot be duplicate +configure.model.openAiCompatible.baseUrl.label=Base url +configure.model.openAiCompatible.apiKey.label=Apikey +configure.model.openAiCompatible.defaultModel.label=Default model +configure.model.openAiCompatible.baseUrl.error.required=Base url is required +configure.model.openAiCompatible.baseUrl.error.validUrl=Base url must be a valid URL +configure.model.openAiCompatible.baseUrl.error.scheme=Base url must start with http:// or https:// +configure.model.openAiCompatible.baseUrl.error.host=Base url must include a valid host +configure.model.openAiCompatible.baseUrl.error.userInfo=Base url must not include user info +configure.model.openAiCompatible.baseUrl.error.fragment=Base url must not include fragment + +sourceBuild.info.cloning=Cloning {0} source from {1} +sourceBuild.error.cloneFailed=Failed to clone {0} source from {1} +sourceBuild.info.building=Building {0} +sourceBuild.error.buildFailed=Failed to build {0}. +sourceBuild.error.artifactNotFound=Could not find built {0} artifact in {1} +sourceBuild.success.installed={0} installed at {1} +sourceBuild.tool.missing.header=Missing required build tools: +sourceBuild.tool.missing.item={0}: +sourceBuild.tool.missing.reason= {0} +sourceBuild.tool.missing.footer=Install the missing tools, then rerun: +sourceBuild.tool.java.reason=Required to run Maven. Command failed: java --version +sourceBuild.tool.javac.reason=Required to compile Partner from source. Install a JDK, not just a JRE. Command failed: javac --version +sourceBuild.tool.git.reason=Required to clone Partner source. Command failed: git --version +sourceBuild.tool.mvn.reason=Required to build Partner from source. Command failed: mvn --version diff --git a/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties b/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties new file mode 100644 index 00000000..54dad2c4 --- /dev/null +++ b/PartnerCtl/src/main/resources/i18n/messages_zh_CN.properties @@ -0,0 +1,91 @@ +init.home.section=初始化 Partner Home +init.home.label=Partner Home +init.home.success=Partner Home 已初始化:{0} +init.home.duplicate.warning=Partner Home 已包含文件:{0} +init.home.duplicate.choice.label=Partner Home 已包含文件,请选择如何继续 +init.home.duplicate.choice.another=使用另一个 Partner Home +init.home.duplicate.choice.another.description=重新选择一个目录 +init.home.duplicate.choice.overwrite=覆盖当前 Partner Home +init.home.duplicate.choice.overwrite.description=删除现有内容并继续 +init.home.duplicate.choice.exit=取消初始化 +init.home.duplicate.confirmDelete=确定删除 {0} 下的所有文件? +init.home.notDirectory=Partner Home 不是目录:{0} +init.home.overwrite.refuseRoot=拒绝覆盖文件系统根目录:{0} +init.home.overwrite.refuseUserHome=拒绝覆盖用户主目录:{0} +init.home.overwrite.refuseBroadDirectory=拒绝覆盖范围过大的目录:{0} +init.install.section=安装 Partner +init.install.method.label=选择安装方式 +init.install.method.buildFromSource=从源码构建 Partner +init.gateway.section=配置网关 +init.gateway.select.label=选择网关 +init.gateway.websocket.choice=WebSocket Gateway +init.gateway.warn.notFound=找不到 id 为 {0} 的网关 +init.gateway.warn.skipped=网关:{0} 配置已跳过 +init.gateway.info.skippedUseDefault=已跳过网关配置。Partner 将使用 WebSocket 作为默认网关 +init.gateway.defaultChannel.label=设置默认 channel +init.gateway.info.defaultChannel=默认 channel 将设置为 {0} +init.gateway.success.configWritten=网关配置已写入 {0} +init.model.section=配置模型 +init.model.provider.default.label=选择默认模型 provider 类型 +init.model.provider.label=选择模型 provider 类型 +init.model.warn.noDefaultProvider=没有配置默认模型 provider。除非 model.json 已存在,或运行时提供 PARTNER_DEFAULT_BASE_URL、PARTNER_DEFAULT_API_KEY 和 PARTNER_DEFAULT_MODEL,否则 Partner 可能无法正常启动。 +init.model.confirm.skipConfiguration=跳过模型配置? +init.model.confirm.addAdditionalProvider=添加额外模型 provider? +init.model.success.configWritten=模型配置已写入 {0} +init.finish.section=完成 +init.finish.confirm.startNow=现在启动 Partner? +init.finish.info.completed=初始化完成。 +init.finish.info.starting=正在启动 Partner... + +configure.gateway.websocket.section=配置网关:WebSocket Gateway +configure.gateway.websocket.port.label=端口 +configure.gateway.websocket.port.error.int=WebSocket 端口只接受整数 +configure.gateway.websocket.port.error.range=WebSocket 端口应在 1 到 65565 之间 +configure.gateway.websocket.heartbeatInterval.label=心跳间隔 +configure.gateway.websocket.heartbeatInterval.error.long=心跳间隔只接受 long 数值 +configure.gateway.websocket.hostname.label=监听主机名 +configure.gateway.websocket.hostname.error.required=主机名不能为空 +configure.gateway.websocket.hostname.error.whitespace=主机名不能包含空白字符。 +configure.gateway.websocket.hostname.error.protocol=不要包含协议。只填写主机名,例如:127.0.0.1 +configure.gateway.websocket.hostname.error.path=不要包含路径。只填写主机名。 +configure.gateway.websocket.hostname.error.port=不要在这里填写端口。端口会单独配置。 +configure.gateway.external.section=配置网关:{0} +configure.gateway.external.details.title=网关模块详情 +configure.gateway.external.details.description=描述 +configure.gateway.external.details.source=来源 +configure.gateway.external.details.buildCommand=构建命令 +configure.gateway.external.details.artifact=构建产物 +configure.gateway.external.details.installTarget=安装目标 +configure.gateway.external.details.configTarget=配置目标 +configure.gateway.external.details.noConfig=无配置 +configure.gateway.external.confirmContinue=继续安装? +configure.field.error.int={0} 只接受整数 +configure.field.error.number={0} 只接受数字 +configure.field.error.boolean={0} 只接受 true 或 false +configure.field.error.rawJson={0} 只接受合法 JSON +configure.model.openAiCompatible.providerName.label=Provider 名称 +configure.model.openAiCompatible.providerName.error.duplicateDefault=Default provider 不能重复 +configure.model.openAiCompatible.baseUrl.label=Base url +configure.model.openAiCompatible.apiKey.label=Apikey +configure.model.openAiCompatible.defaultModel.label=Default model +configure.model.openAiCompatible.baseUrl.error.required=Base url 不能为空 +configure.model.openAiCompatible.baseUrl.error.validUrl=Base url 必须是合法 URL +configure.model.openAiCompatible.baseUrl.error.scheme=Base url 必须以 http:// 或 https:// 开头 +configure.model.openAiCompatible.baseUrl.error.host=Base url 必须包含合法 host +configure.model.openAiCompatible.baseUrl.error.userInfo=Base url 不能包含 user info +configure.model.openAiCompatible.baseUrl.error.fragment=Base url 不能包含 fragment + +sourceBuild.info.cloning=正在从 {1} 克隆 {0} 源码 +sourceBuild.error.cloneFailed=从 {1} 克隆 {0} 源码失败 +sourceBuild.info.building=正在构建 {0} +sourceBuild.error.buildFailed=构建 {0} 失败。 +sourceBuild.error.artifactNotFound=在 {1} 中找不到已构建的 {0} 产物 +sourceBuild.success.installed={0} 已安装到 {1} +sourceBuild.tool.missing.header=缺少必要的构建工具: +sourceBuild.tool.missing.item={0}: +sourceBuild.tool.missing.reason= {0} +sourceBuild.tool.missing.footer=安装缺失工具后重新运行: +sourceBuild.tool.java.reason=运行 Maven 需要 java。命令失败:java --version +sourceBuild.tool.javac.reason=从源码编译 Partner 需要 javac。请安装 JDK,而不只是 JRE。命令失败:javac --version +sourceBuild.tool.git.reason=克隆 Partner 源码需要 git。命令失败:git --version +sourceBuild.tool.mvn.reason=从源码构建 Partner 需要 mvn。命令失败:mvn --version