From 857c68e502e7ff30ab8de0a9e2dfb659e4c01e83 Mon Sep 17 00:00:00 2001 From: Ilia Permiashkin Date: Mon, 9 Mar 2026 14:51:58 +0000 Subject: [PATCH] [grazie] IJPL-236018 Russian spelling rule names are written in Russian and in English Merge-request: IJ-MR-195145 Merged-by: Ilia Permiashkin GitOrigin-RevId: 483219ae67c264c74ea8f1201719b9b31f8db5a5 --- .../src/com/intellij/DynamicBundle.java | 68 ++++++++++++------- ...amicBundleLocaleCacheTestBundle.properties | 1 + ...cBundleLocaleCacheTestBundle_en.properties | 1 + ...cBundleLocaleCacheTestBundle_ru.properties | 1 + .../intellij/DynamicBundleLocaleCacheTest.kt | 53 +++++++++++++++ .../com/intellij/grazie/GrazieDynamic.kt | 12 ++-- 6 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle.properties create mode 100644 platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_en.properties create mode 100644 platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_ru.properties create mode 100644 platform/util/testSrc/com/intellij/DynamicBundleLocaleCacheTest.kt diff --git a/platform/core-api/src/com/intellij/DynamicBundle.java b/platform/core-api/src/com/intellij/DynamicBundle.java index c45d515b8227..29f7ab1d2df1 100644 --- a/platform/core-api/src/com/intellij/DynamicBundle.java +++ b/platform/core-api/src/com/intellij/DynamicBundle.java @@ -76,29 +76,30 @@ public class DynamicBundle extends AbstractBundle { protected @NotNull ResourceBundle findBundle(@NotNull String pathToBundle, @NotNull ClassLoader baseLoader, @NotNull ResourceBundle.Control control) { + Locale locale = getResolveLocale(); return (DefaultBundleService.isDefaultBundle() ? ourDefaultCache : ourCache) .computeIfAbsent(baseLoader, __ -> CollectionFactory.createConcurrentSoftValueMap()) - .computeIfAbsent(pathToBundle, __ -> + .computeIfAbsent(new BundleCacheKey(pathToBundle, locale), __ -> resolveResourceBundle( getBundleClassLoader(), baseLoader, pathToBundle, - getResolveLocale(), - (loader, locale) -> super.findBundle(pathToBundle, loader, control, locale) + locale, + (loader, resolvedLocale) -> super.findBundle(pathToBundle, loader, control, resolvedLocale) )); } @Nullable - private static ResourceBundle getBundleFromCache(@NotNull ClassLoader loader, @NotNull String pathToBundle) { - Map loaderCache = ourCache.get(loader); + private static ResourceBundle getBundleFromCache(@NotNull ClassLoader loader, @NotNull String pathToBundle, @NotNull Locale locale) { + Map loaderCache = ourCache.get(loader); if (loaderCache == null) return null; - return loaderCache.get(pathToBundle); + return loaderCache.get(new BundleCacheKey(pathToBundle, locale)); } - private static void removeBundleFromCache(@NotNull ClassLoader loader, @NotNull String pathToBundle) { - Map loaderCache = ourCache.get(loader); + private static void removeBundleFromCache(@NotNull ClassLoader loader, @NotNull String pathToBundle, @NotNull Locale locale) { + Map loaderCache = ourCache.get(loader); if (loaderCache == null) return; - loaderCache.remove(pathToBundle); + loaderCache.remove(new BundleCacheKey(pathToBundle, locale)); } private static @NotNull ResourceBundle resolveResourceBundle(@NotNull ClassLoader bundleClassLoader, @@ -112,7 +113,7 @@ public class DynamicBundle extends AbstractBundle { Map bundleOrderMap = new HashMap<>(); if (pluginClassLoader != null) { try { - ResourceBundle pluginBundle = bundleResolver.apply(pluginClassLoader, Locale.ROOT); + ResourceBundle pluginBundle = bundleResolver.apply(pluginClassLoader, locale); bundleOrderMap.put(LocalizationOrder.DEFAULT_PLUGIN, pluginBundle); } catch (MissingResourceException e) { @@ -244,14 +245,15 @@ public class DynamicBundle extends AbstractBundle { LOG.warn("Bundle without name cannot be properly cached: " + bundle); return bundle; } - ResourceBundle bundleFromCache = getBundleFromCache(classLoader, pathToBundle); + Locale locale = getResolveLocale(); + ResourceBundle bundleFromCache = getBundleFromCache(classLoader, pathToBundle, locale); if (bundleFromCache == null) { // Return null to force findBundle execution which will properly resolve and cache the bundle return null; } if (bundleFromCache != bundle) { LOG.info("Cleanup bundle cache for " + pathToBundle); - removeBundleFromCache(classLoader, pathToBundle); + removeBundleFromCache(classLoader, pathToBundle, locale); return null; } return bundle; @@ -298,30 +300,50 @@ public class DynamicBundle extends AbstractBundle { } } - private static final Map> ourCache = CollectionFactory.createConcurrentWeakMap(); - private static final Map> ourDefaultCache = CollectionFactory.createConcurrentWeakMap(); + private static final class BundleCacheKey { + private final @NotNull String pathToBundle; + private final @NotNull Locale locale; + + private BundleCacheKey(@NotNull String pathToBundle, @NotNull Locale locale) { + this.pathToBundle = pathToBundle; + this.locale = locale; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof BundleCacheKey)) return false; + BundleCacheKey key = (BundleCacheKey)obj; + return pathToBundle.equals(key.pathToBundle) && locale.equals(key.locale); + } + + @Override + public int hashCode() { + int result = pathToBundle.hashCode(); + result = 31 * result + locale.hashCode(); + return result; + } + } + + private static final Map> ourCache = CollectionFactory.createConcurrentWeakMap(); + private static final Map> ourDefaultCache = CollectionFactory.createConcurrentWeakMap(); public static @NotNull ResourceBundle getResourceBundle(@NotNull ClassLoader loader, @NotNull @NonNls String pathToBundle) { + Locale locale = getResolveLocale(); return (DefaultBundleService.isDefaultBundle() ? ourDefaultCache : ourCache) .computeIfAbsent(loader, __ -> CollectionFactory.createConcurrentSoftValueMap()) - .computeIfAbsent(pathToBundle, __ -> resolveResourceBundle(loader, pathToBundle, getResolveLocale())); + .computeIfAbsent(new BundleCacheKey(pathToBundle, locale), __ -> resolveResourceBundle(loader, pathToBundle, locale)); } public static @NotNull ResourceBundle getResourceBundle(@NotNull ClassLoader loader, @NotNull @NonNls String pathToBundle, @NotNull Locale locale) { return (DefaultBundleService.isDefaultBundle() ? ourDefaultCache : ourCache) .computeIfAbsent(loader, __ -> CollectionFactory.createConcurrentSoftValueMap()) - .computeIfAbsent(pathToBundle, __ -> resolveResourceBundle(loader, pathToBundle, locale)); + .computeIfAbsent(new BundleCacheKey(pathToBundle, locale), __ -> resolveResourceBundle(loader, pathToBundle, locale)); } @ApiStatus.Internal public static @NotNull ResourceBundle getResourceBundleLocalized(@NotNull ClassLoader loader, @NotNull @NonNls String pathToBundle, @NotNull Locale locale) { - ResourceBundle bundle = (DefaultBundleService.isDefaultBundle() ? ourDefaultCache : ourCache) - .computeIfAbsent(loader, __ -> CollectionFactory.createConcurrentSoftValueMap()) - .get(pathToBundle); - if (bundle != null && bundle.getLocale().equals(locale)) { - return bundle; - } - return resolveResourceBundle(loader, pathToBundle, locale); + return getResourceBundle(loader, pathToBundle, locale); } public static @Nullable ResourceBundle getPluginBundle(@NotNull PluginDescriptor pluginDescriptor) { diff --git a/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle.properties b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle.properties new file mode 100644 index 000000000000..b8e5a97e4c4e --- /dev/null +++ b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle.properties @@ -0,0 +1 @@ +value=root diff --git a/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_en.properties b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_en.properties new file mode 100644 index 000000000000..d9a850c24941 --- /dev/null +++ b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_en.properties @@ -0,0 +1 @@ +value=en diff --git a/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_ru.properties b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_ru.properties new file mode 100644 index 000000000000..c3a577819a0f --- /dev/null +++ b/platform/util/testResources/com/intellij/DynamicBundleLocaleCacheTestBundle_ru.properties @@ -0,0 +1 @@ +value=ru diff --git a/platform/util/testSrc/com/intellij/DynamicBundleLocaleCacheTest.kt b/platform/util/testSrc/com/intellij/DynamicBundleLocaleCacheTest.kt new file mode 100644 index 000000000000..9599fa6cf446 --- /dev/null +++ b/platform/util/testSrc/com/intellij/DynamicBundleLocaleCacheTest.kt @@ -0,0 +1,53 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import java.util.Locale +import java.util.ResourceBundle + +class DynamicBundleLocaleCacheTest { + @BeforeEach + fun setUp() { + DynamicBundle.clearCache() + ResourceBundle.clearCache(javaClass.classLoader) + } + + @AfterEach + fun tearDown() { + DynamicBundle.clearCache() + ResourceBundle.clearCache(javaClass.classLoader) + } + + @Test + fun `explicit locale lookups are cached per locale`() { + val loader = javaClass.classLoader + + val englishBundle = DynamicBundle.getResourceBundle(loader, BUNDLE_NAME, Locale.ENGLISH) + val russianBundle = DynamicBundle.getResourceBundle(loader, BUNDLE_NAME, Locale.forLanguageTag("ru")) + + assertThat(englishBundle.getString(MESSAGE_KEY)).isEqualTo("en") + assertThat(russianBundle.getString(MESSAGE_KEY)).isEqualTo("ru") + } + + @ParameterizedTest + @ValueSource(strings = ["ru", "en"]) + fun `default locale lookup does not poison explicit locale cache`(localeVariant: String) { + val loader = javaClass.classLoader + val locale = Locale.forLanguageTag(localeVariant) + + DynamicBundle.getResourceBundle(loader, BUNDLE_NAME) + val explicitBundle = DynamicBundle.getResourceBundle(loader, BUNDLE_NAME, locale) + + assertThat(explicitBundle.getString(MESSAGE_KEY)).isEqualTo(localeVariant) + } + + companion object { + private const val BUNDLE_NAME = "com.intellij.DynamicBundleLocaleCacheTestBundle" + private const val MESSAGE_KEY = "value" + } +} diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/GrazieDynamic.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/GrazieDynamic.kt index eadd1be7f27f..fa4b61044b29 100644 --- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/GrazieDynamic.kt +++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/GrazieDynamic.kt @@ -166,7 +166,8 @@ object GrazieDynamic : DynamicPluginListener { fun getResourceBundle(baseName: String, locale: Locale): ResourceBundle { return forClassLoader { try { - DynamicBundle.getResourceBundle(GrazieBundle::class.java.classLoader, baseName) + DynamicBundle.getResourceBundle(it, baseName, locale) + .takeIf { bundle -> bundle.locale.language == locale.language } } catch (e: MissingResourceException) { null @@ -174,11 +175,6 @@ object GrazieDynamic : DynamicPluginListener { } ?: throw MissingResourceException("Missing resource bundle for $baseName with locale $locale", GrazieDynamic.javaClass.name, baseName) } - private inline fun forClassLoader(crossinline body: (ClassLoader) -> T?): T? { - return body(GraziePlugin.classLoader) ?: dynClassLoaders - .asSequence() - .mapNotNull { - body(it) - }.firstOrNull() - } + private inline fun forClassLoader(crossinline body: (ClassLoader) -> T?): T? = + body(GraziePlugin.classLoader) ?: dynClassLoaders.firstNotNullOfOrNull { body(it) } }