feat(partnerctl-init): add i18n message bundles for init flow and source build output

This commit is contained in:
2026-05-05 22:37:41 +08:00
parent 0710f60ed6
commit 9073f88117
7 changed files with 294 additions and 89 deletions

View File

@@ -115,6 +115,7 @@
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
<buildArg>--initialize-at-build-time=kotlin.DeprecationLevel</buildArg>
<buildArg>-H:IncludeResourceBundles=i18n.messages</buildArg>
</buildArgs>
</configuration>
</plugin>

View File

@@ -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()),

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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, String>): 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<String, String>()
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))

View File

@@ -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

View File

@@ -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