diff --git a/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java b/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java index e2c0fba3e649..9cfc2d276d18 100644 --- a/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/options/ex/ConfigurableExtensionPointUtil.java @@ -1,7 +1,7 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.options.ex; -import com.intellij.BundleBase; +import com.intellij.IntelliJResourceBundle; import com.intellij.ide.actions.ConfigurablesPatcher; import com.intellij.ide.ui.search.SearchableOptionsRegistrar; import com.intellij.openapi.application.Application; @@ -226,7 +226,7 @@ public final class ConfigurableExtensionPointUtil { LOG.warn("Use to specify custom configurable group: " + groupId); int weight = getInt(bundle, id + ".settings.weight"); String help = getString(bundle, id + ".settings.help.topic"); - String name = getName(bundle, id + ".settings.display.name"); + String name = getString(bundle, id + ".settings.display.name"); String desc = getString(bundle, id + ".settings.description"); if (name != null && project != null) { if (!project.isDefault() && !name.contains("{")) { @@ -237,7 +237,7 @@ public final class ConfigurableExtensionPointUtil { name = StringUtil.first(MessageFormat.format(name, project.getName()), 30, true); } } - node.myValue = new SortedConfigurableGroup(id, name, desc, help, weight); + node.myValue = new SortedConfigurableGroup(id, Objects.requireNonNullElse(name, ""), desc, help, weight); } } if (configurables != null) { @@ -433,20 +433,17 @@ public final class ConfigurableExtensionPointUtil { return null; } - private static @Nls String getString(ResourceBundle bundle, @NonNls String resource) { - if (bundle == null) return null; - try { - return bundle.getString(resource); - } - catch (MissingResourceException ignored) { + private static @Nls @Nullable String getString(ResourceBundle bundle, @NonNls String resource) { + if (bundle == null) { return null; } - } - private static @Nls String getName(ResourceBundle bundle, @NonNls String resource) { - if (bundle == null) return null; + if (bundle instanceof IntelliJResourceBundle b) { + return b.getMessageOrNull(resource); + } + try { - return BundleBase.messageOrDefault(bundle, resource, null); + return bundle.getString(resource); } catch (MissingResourceException ignored) { return null; diff --git a/platform/util/api-dump-unreviewed.txt b/platform/util/api-dump-unreviewed.txt index 7101ff838a58..87ad5c94f591 100644 --- a/platform/util/api-dump-unreviewed.txt +++ b/platform/util/api-dump-unreviewed.txt @@ -31,7 +31,6 @@ f:com.intellij.BundleBase - sf:partialMessage(java.util.ResourceBundle,java.lang.String,I,java.lang.Object[]):java.lang.String - sf:replaceMnemonicAmpersand(java.lang.String):java.lang.String - sf:setTranslationConsumer(java.util.function.BiConsumer):V -- sf:useDefaultValue(java.util.ResourceBundle,java.lang.String,java.lang.String):java.lang.String f:com.intellij.Patches - sf:IBM_JDK_DISABLE_COLLECTION_BUG:Z - sf:JDK_BUG_ID_21275177:Z diff --git a/platform/util/src/com/intellij/AbstractBundle.kt b/platform/util/src/com/intellij/AbstractBundle.kt index b3ebdf479bb5..160df08e27ed 100644 --- a/platform/util/src/com/intellij/AbstractBundle.kt +++ b/platform/util/src/com/intellij/AbstractBundle.kt @@ -4,7 +4,6 @@ package com.intellij import com.intellij.BundleBase.partialMessage -import com.intellij.BundleBase.useDefaultValue import com.intellij.openapi.diagnostic.logger import com.intellij.util.ArrayUtilRt import com.intellij.util.DefaultBundleService @@ -54,14 +53,14 @@ open class AbstractBundle { fun messageOrDefault(bundle: ResourceBundle?, key: @NonNls String, defaultValue: @Nls String?, vararg params: Any?): @Nls String? { return when { bundle == null -> defaultValue - !bundle.containsKey(key) -> postprocessValue(bundle = bundle, value = useDefaultValue(bundle, key, defaultValue), params = params) - else -> BundleBase.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) + !bundle.containsKey(key) -> postprocessValue(bundle = bundle, value = defaultValue ?: useDefaultValue(bundle, key), params = params) + else -> com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) } } @JvmStatic fun message(bundle: ResourceBundle, key: @NonNls String, vararg params: Any?): @Nls String { - return BundleBase.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) } @Suppress("HardCodedStringLiteral") @@ -75,7 +74,7 @@ open class AbstractBundle { inline fun resolveResourceBundleWithFallback( loader: ClassLoader, pathToBundle: String, - firstTry: () -> ResourceBundle + firstTry: () -> ResourceBundle, ): ResourceBundle { try { return firstTry() @@ -96,7 +95,7 @@ open class AbstractBundle { @Contract(pure = true) // open only to preserve compatibility open fun getMessage(key: @NonNls String, vararg params: Any?): @Nls String { - return BundleBase.messageOrDefault(bundle = resourceBundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = resourceBundle, key = key, defaultValue = null, params = params) } /** @@ -202,7 +201,7 @@ private object IntelliJResourceControl : ResourceBundle.Control() { val resourceName = (if (bundleName.contains("://")) null else toResourceName(bundleName, "properties")) ?: return null val stream = loader.getResourceAsStream(resourceName) ?: return null return stream.use { - PropertyResourceBundle(InputStreamReader(it, StandardCharsets.UTF_8)) + IntelliJResourceBundle(InputStreamReader(it, StandardCharsets.UTF_8)) } } -} +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/BundleBase.kt b/platform/util/src/com/intellij/BundleBase.kt index f311af96b2fc..88c94528759e 100644 --- a/platform/util/src/com/intellij/BundleBase.kt +++ b/platform/util/src/com/intellij/BundleBase.kt @@ -1,6 +1,12 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplaceGetOrSet") + package com.intellij +import com.intellij.BundleBase.L10N_MARKER +import com.intellij.BundleBase.SHOW_LOCALIZED_MESSAGES +import com.intellij.BundleBase.appendLocalizationSuffix +import com.intellij.BundleBase.getDefaultMessage import com.intellij.BundleBase.replaceMnemonicAmpersand import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.logger @@ -25,15 +31,21 @@ private val SUFFIXES = arrayOf("", "") private var translationConsumer: BiConsumer? = null private val SHOW_DEFAULT_MESSAGES: Boolean = java.lang.Boolean.getBoolean("idea.l10n.english") -// 0: no, 1: show key, 2: only keys -private val SHOW_KEYS: Int = System.getProperty("idea.l10n.keys").let { + +private val getPolicy: GetPolicy = System.getProperty("idea.l10n.keys").let { when (it) { - "true" -> 1 - "only" -> 2 - else -> 0 + "true" -> GetPolicy.APPEND_KEY + "only" -> GetPolicy.ONLY_KEY + else -> GetPolicy.VALUE } } +private enum class GetPolicy { + VALUE, + APPEND_KEY, + ONLY_KEY, +} + object BundleBase { const val MNEMONIC: Char = 0x1B.toChar() const val MNEMONIC_STRING: @NlsSafe String = MNEMONIC.toString() @@ -79,43 +91,17 @@ object BundleBase { @JvmStatic fun message(bundle: ResourceBundle, key: String, vararg params: Any): @Nls String { - return messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = null, params = params) } + @Suppress("DuplicatedCode") @JvmStatic fun messageOrDefault(bundle: ResourceBundle?, key: String, defaultValue: @Nls String?, vararg params: Any?): @Nls String { if (bundle == null) { return defaultValue!! } - var resourceFound = true - val value = try { - bundle.getString(key) - } - catch (e: MissingResourceException) { - resourceFound = false - useDefaultValue(bundle = bundle, key = key, defaultValue = defaultValue) - } - - if (SHOW_KEYS == 2) { - @Suppress("HardCodedStringLiteral") - return key - } - - val result = postprocessValue(bundle = bundle, value = value, params = params) - translationConsumer?.accept(key, result) - return when { - !resourceFound -> result - SHOW_KEYS == 1 && SHOW_DEFAULT_MESSAGES -> { - appendLocalizationSuffix(result = result, suffixToAppend = " ($key=${getDefaultMessage(bundle, key)})") - } - SHOW_KEYS == 1 -> appendLocalizationSuffix(result = result, suffixToAppend = " ($key)") - SHOW_DEFAULT_MESSAGES -> { - appendLocalizationSuffix(result = result, suffixToAppend = " (${getDefaultMessage(bundle, key)})") - } - SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = L10N_MARKER) - else -> result - } + return com.intellij.messageOrDefault(bundle = bundle, key = key, defaultValue = defaultValue, params = params) } @JvmStatic @@ -144,19 +130,6 @@ object BundleBase { return result + suffixToAppend } - @JvmStatic - @Suppress("HardCodedStringLiteral") - fun useDefaultValue(bundle: ResourceBundle?, key: String, defaultValue: @Nls String?): @Nls String { - if (defaultValue != null) { - return defaultValue - } - if (assertOnMissedKeys) { - val bundleName = if (bundle == null) "" else "(${bundle.baseBundleName})" - LOG.error("'$key' is not found in $bundle$bundleName") - } - return "!$key!" - } - @JvmStatic @Contract(pure = true) fun format(value: String, vararg params: Any): String { @@ -244,20 +217,92 @@ private fun quotePattern(message: String): @NlsSafe String { } @Suppress("HardCodedStringLiteral") -internal fun postprocessValue(bundle: ResourceBundle, value: @Nls String, vararg params: Any?): @Nls String { - @Suppress("NAME_SHADOWING") var value = value - value = replaceMnemonicAmpersand(value)!! - if (params.isEmpty() || !value.contains('{')) { +internal fun postprocessValue(bundle: ResourceBundle, value: @Nls String, params: Array?): @Nls String { + @Suppress("NAME_SHADOWING") val value = replaceMnemonicAmpersand(value)!! + if (params.isNullOrEmpty() || !value.contains('{')) { return value } val locale = bundle.locale - return try { + try { val format = if (locale == null) MessageFormat(value) else MessageFormat(value, locale) OrdinalFormat.apply(format) - format.format(params) + return format.format(params) } catch (e: IllegalArgumentException) { - "!invalid format: `$value`!" + return "!invalid format: `$value`!" + } +} + +@Suppress("DuplicatedCode") +@Internal +fun messageOrDefault(bundle: ResourceBundle, key: String, defaultValue: @Nls String?, params: Array?): @Nls String { + if (bundle !is IntelliJResourceBundle || bundle.parent != null) { + @Suppress("HardCodedStringLiteral") + return messageOrDefaultForJdkBundle(bundle = bundle, key = key, defaultValue = defaultValue, params = params) + } + + return bundle.getMessage(key = key, defaultValue = defaultValue, params = params) +} + +@Suppress("DuplicatedCode") +private fun messageOrDefaultForJdkBundle( + bundle: ResourceBundle, + key: String, + defaultValue: @Nls String?, + params: Array?, +): String { + var resourceFound = true + val value = try { + bundle.getString(key) + } + catch (e: MissingResourceException) { + resourceFound = false + defaultValue ?: useDefaultValue(bundle = bundle, key = key) + } + + val result = postprocessValue(bundle = bundle, value = value, params = params) + translationConsumer?.accept(key, result) + return when { + !resourceFound -> result + getPolicy == GetPolicy.APPEND_KEY -> { + appendLocalizationSuffix( + result = result, + suffixToAppend = if (SHOW_DEFAULT_MESSAGES) " ($key=${getDefaultMessage(bundle, key)})" else " ($key)", + ) + } + SHOW_DEFAULT_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = " (${getDefaultMessage(bundle, key)})") + SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = result, suffixToAppend = L10N_MARKER) + else -> result + } +} + +internal fun useDefaultValue(bundle: ResourceBundle, @NlsSafe key: String): @Nls String { + if (assertOnMissedKeys) { + LOG.error("'$key' is not found (baseBundleName=${bundle.baseBundleName}, bundle=$bundle)") + } + return "!$key!" +} + +internal fun postProcessResolvedValue( + @NlsSafe value: String, + key: String, + resourceFound: Boolean, + bundle: ResourceBundle, +): String { + translationConsumer?.accept(key, value) + return when { + !resourceFound -> value + getPolicy == GetPolicy.ONLY_KEY -> "$key${L10N_MARKER}$value" + getPolicy == GetPolicy.APPEND_KEY -> { + appendLocalizationSuffix( + result = value, + suffixToAppend = if (SHOW_DEFAULT_MESSAGES) " ($key=${getDefaultMessage(bundle, key)})" else " ($key)", + ) + } + SHOW_DEFAULT_MESSAGES -> appendLocalizationSuffix(result = value, + suffixToAppend = " (${getDefaultMessage(bundle, key)})") + SHOW_LOCALIZED_MESSAGES -> appendLocalizationSuffix(result = value, suffixToAppend = L10N_MARKER) + else -> value } } diff --git a/platform/util/src/com/intellij/IntelliJResourceBundle.kt b/platform/util/src/com/intellij/IntelliJResourceBundle.kt new file mode 100644 index 000000000000..cd65e373203d --- /dev/null +++ b/platform/util/src/com/intellij/IntelliJResourceBundle.kt @@ -0,0 +1,99 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("ReplaceGetOrSet") + +package com.intellij + +import com.intellij.openapi.util.NlsSafe +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls +import java.io.Reader +import java.util.* + +@ApiStatus.Internal +class IntelliJResourceBundle internal constructor(reader: Reader) : ResourceBundle() { + private val lookup: Map + + init { + val properties = Properties() + properties.load(reader) + // don't use immutable map - HashMap performance is better (with String keys) + @Suppress("UNCHECKED_CAST") + lookup = HashMap(properties as Map) + } + + val parent: ResourceBundle? + get() = super.parent + + @NlsSafe + fun getMessageOrNull(key: String): String? { + val value = lookup.get(key) ?: return null + return postProcessResolvedValue(value = value, key = key, resourceFound = true, bundle = this) + } + + @NlsSafe + fun getMessage(key: String, defaultValue: @Nls String?, params: Array?): String { + @Suppress("HardCodedStringLiteral") + var value = lookup.get(key) + val resourceFound = if (value == null) { + value = defaultValue ?: useDefaultValue(bundle = this, key = key) + false + } + else { + true + } + + return postProcessResolvedValue( + value = postprocessValue(bundle = this, value = value, params = params), + key = key, + resourceFound = resourceFound, + bundle = this, + ) + } + + // UI Designer uses ResourceBundle directly, via Java API. + // `getMessage` is not called, so we have to provide our own implementation of ResourceBundle + override fun handleGetObject(key: String): String? = getMessageOrNull(key = key) + + override fun getKeys(): Enumeration { + val parent = super.parent + return if (parent == null) Collections.enumeration(lookup.keys) else ResourceBundleWithParentEnumeration(lookup.keys, parent.keys) + } + + override fun handleKeySet(): Set = lookup.keys +} + +private class ResourceBundleWithParentEnumeration( + private var set: Set, + private var enumeration: Enumeration, +) : Enumeration { + private var iterator: Iterator = set.iterator() + private var next: String? = null + + override fun hasMoreElements(): Boolean { + if (next == null) { + if (iterator.hasNext()) { + next = iterator.next() + } + else { + while (next == null && enumeration.hasMoreElements()) { + next = enumeration.nextElement() + if (set.contains(next)) { + next = null + } + } + } + } + return next != null + } + + override fun nextElement(): String? { + if (hasMoreElements()) { + val result = next + next = null + return result + } + else { + throw NoSuchElementException() + } + } +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/UtilBundle.java b/platform/util/src/com/intellij/UtilBundle.java index 7bd89e54c7d6..0556ccb5d1f7 100644 --- a/platform/util/src/com/intellij/UtilBundle.java +++ b/platform/util/src/com/intellij/UtilBundle.java @@ -1,4 +1,4 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij; import org.jetbrains.annotations.Nls; @@ -14,7 +14,7 @@ public final class UtilBundle { private UtilBundle() { } public static @NotNull @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, Object @NotNull ... params) { - return BundleBase.messageOrDefault(getUtilBundle(), key, null, params); + return BundleBaseKt.messageOrDefault(getUtilBundle(), key, null, params); } private static @NotNull ResourceBundle getUtilBundle() { diff --git a/plugins/editorconfig/src/org/editorconfig/Utils.kt b/plugins/editorconfig/src/org/editorconfig/Utils.kt index c16a4a20b40e..eb3356a9dc1d 100644 --- a/plugins/editorconfig/src/org/editorconfig/Utils.kt +++ b/plugins/editorconfig/src/org/editorconfig/Utils.kt @@ -1,4 +1,4 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.editorconfig import com.intellij.BundleBase @@ -22,11 +22,7 @@ import com.intellij.psi.search.FileTypeIndex import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.LineSeparator import org.ec4j.core.ResourceProperties -import org.editorconfig.configmanagement.ConfigEncodingCharsetUtil -import org.editorconfig.configmanagement.StandardEditorConfigProperties -import org.editorconfig.configmanagement.indentSizeKey -import org.editorconfig.configmanagement.indentStyleKey -import org.editorconfig.configmanagement.tabWidthKey +import org.editorconfig.configmanagement.* import org.editorconfig.language.messages.EditorConfigBundle import org.editorconfig.plugincomponents.EditorConfigPropertiesService import org.editorconfig.settings.EditorConfigSettings @@ -81,9 +77,12 @@ object Utils { fun invalidConfigMessage(project: Project, configValue: String?, configKey: String, filePath: String?) { val message = if (configValue != null) { - BundleBase.messageOrDefault(EditorConfigBundle.bundle.resourceBundle, "invalid.config.value", + BundleBase.messageOrDefault(EditorConfigBundle.bundle.resourceBundle, + "invalid.config.value", null, - configValue, configKey.ifEmpty { "?" }, filePath) + configValue, + configKey.ifEmpty { "?" }, + filePath) } else { EditorConfigBundle.message("read.failure")