Skip to content

Commit 9fa045c

Browse files
jreznotintellij-monorepo-bot
authored andcommitted
[java-i18n] IDEA-383845 Fix StringUtil.wordsToBeginFromUpperCase with prepositions in Title capitalization
GitOrigin-RevId: 4efb81ba5bdb8143b35892c5262a75bc33f6d1c7
1 parent 6aaa69a commit 9fa045c

File tree

4 files changed

+31
-210
lines changed

4 files changed

+31
-210
lines changed

jvm/jvm-analysis-api/src/com/intellij/codeInspection/NlsCapitalizationUtil.java

Lines changed: 6 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -5,86 +5,19 @@
55
import org.jetbrains.annotations.Nls;
66
import org.jetbrains.annotations.NotNull;
77

8-
import java.util.*;
9-
import java.util.regex.Pattern;
8+
import java.util.List;
109

1110
public final class NlsCapitalizationUtil {
12-
private static final Set<String> TITLE_CASE_LOWERCASE_WORDS = Set.of(
13-
"a", "an", "the",
14-
"and", "or", "but",
15-
"at", "by", "for", "from", "in", "into", "of", "off", "on", "onto", "out", "over", "to", "up", "with"
16-
);
17-
private static final Pattern PERIOD_PATTERN = Pattern.compile("\\.(?!\\s*$)");
18-
private static final Pattern DOUBLE_QUOTES_PATTERN = Pattern.compile("[“”\"]");
19-
private static final Pattern EXCLAMATION_PATTERN = Pattern.compile("!");
20-
private static final Pattern CONTRACTION_PATTERN = Pattern.compile("(?i)\\b(can't|won't|isn't|aren't|wasn't|weren't|hasn't|haven't|hadn't|doesn't|don't|didn't|shouldn't|wouldn't|couldn't|mightn't|mustn't)\\b(?<!Don't)");
21-
private static final Pattern WHITESPACE_SPLIT_PATTERN = Pattern.compile("(?<=\\s)|(?=\\s)");
22-
private static final Pattern PUNCTUATION_WITH_WORD_PATTERN = Pattern.compile("(^\\P{Alnum}*)([\\p{Alnum}]+)(\\P{Alnum}*$)");
23-
private static final Pattern LEADING_PUNCTUATION_WITH_FIRST_LETTER_PATTERN = Pattern.compile("(^\\P{Alnum}*)(\\p{Alpha})(.*)");
24-
private static final Pattern SPECIAL_PREFIX_PATTERN = Pattern.compile("^[.*~].*");
25-
2611
public static boolean isCapitalizationSatisfied(String value, Nls.Capitalization capitalization) {
2712
if (StringUtil.isEmpty(value) || capitalization == Nls.Capitalization.NotSpecified) {
2813
return true;
2914
}
15+
3016
return capitalization == Nls.Capitalization.Title
31-
? checkTitleCapitalization(value)
17+
? StringUtil.wordsToBeginFromUpperCase(value).equals(value)
3218
: checkSentenceCapitalization(value);
3319
}
3420

35-
private static List<String> splitByWhitespace(String s) {
36-
return Arrays.stream(s.trim().split("\\s+"))
37-
.filter(str -> !str.isEmpty())
38-
.toList();
39-
}
40-
41-
private static boolean checkTitleCapitalization(@NotNull String value) {
42-
List<String> words = splitByWhitespace(value);
43-
final int wordCount = words.size();
44-
if (wordCount == 0) return true;
45-
for (int i = 0; i < wordCount; i++) {
46-
String word = words.get(i);
47-
if (word.isEmpty()) continue;
48-
String cleanWord = stripPunctuation(word);
49-
if (cleanWord.isEmpty()) continue;
50-
// Check if it's a special case (like iOS, macOS)
51-
if (hasInternalCapitalization(cleanWord)) {
52-
continue;
53-
}
54-
if (i == 0 || i == wordCount - 1) {
55-
if (!isCapitalizedWord(cleanWord)) return false;
56-
}
57-
else {
58-
String lowerWord = cleanWord.toLowerCase(Locale.ENGLISH);
59-
if (TITLE_CASE_LOWERCASE_WORDS.contains(lowerWord)) {
60-
if (isCapitalizedWord(cleanWord)) return false;
61-
}
62-
else {
63-
if (!isCapitalizedWord(cleanWord)) return false;
64-
}
65-
}
66-
}
67-
return true;
68-
}
69-
70-
private static boolean hasInternalCapitalization(@NotNull String word) {
71-
if (word.length() <= 1) return false;
72-
boolean hasLowerCase = false;
73-
boolean hasUpperCaseAfterFirst = false;
74-
for (int i = 0; i < word.length(); i++) {
75-
char c = word.charAt(i);
76-
if (Character.isLetter(c)) {
77-
if (Character.isLowerCase(c)) {
78-
hasLowerCase = true;
79-
}
80-
else if (i > 0 && Character.isUpperCase(c)) {
81-
hasUpperCaseAfterFirst = true;
82-
}
83-
}
84-
}
85-
return hasLowerCase && hasUpperCaseAfterFirst;
86-
}
87-
8821
private static boolean checkSentenceCapitalization(@NotNull String value) {
8922
List<String> words = StringUtil.split(value, " ");
9023
final int wordCount = words.size();
@@ -116,98 +49,9 @@ private static boolean isCapitalizedWord(String word) {
11649
return !word.isEmpty() && Character.isLetter(word.charAt(0)) && Character.isUpperCase(word.charAt(0));
11750
}
11851

119-
private static @NotNull String stripPunctuation(@NotNull String word) {
120-
int start = 0;
121-
int end = word.length();
122-
while (start < end && !Character.isLetterOrDigit(word.charAt(start))) {
123-
start++;
124-
}
125-
while (end > start && !Character.isLetterOrDigit(word.charAt(end - 1))) {
126-
end--;
127-
}
128-
return start < end ? word.substring(start, end) : "";
129-
}
130-
131-
public static boolean checkPunctuation(@NotNull String value) {
132-
if (PERIOD_PATTERN.matcher(value).find()) {
133-
return value.endsWith(".");
134-
}
135-
if (value.endsWith(".")) {
136-
return false;
137-
}
138-
if (DOUBLE_QUOTES_PATTERN.matcher(value).find()) {
139-
return false;
140-
}
141-
if (EXCLAMATION_PATTERN.matcher(value).find()) {
142-
return false;
143-
}
144-
if (CONTRACTION_PATTERN.matcher(value).find()) {
145-
return false;
146-
}
147-
return true;
148-
}
149-
15052
public static @NotNull String fixValue(String string, Nls.Capitalization capitalization) {
151-
if (capitalization == Nls.Capitalization.Title) {
152-
return fixTitleCapitalization(string);
153-
}
154-
else {
155-
return StringUtil.capitalize(StringUtil.wordsToBeginFromLowerCase(string));
156-
}
157-
}
158-
159-
private static String fixTitleCapitalization(String text) {
160-
if (text == null || text.isBlank()) return text;
161-
String[] tokens = WHITESPACE_SPLIT_PATTERN.split(text);
162-
163-
int firstWordIndex = -1, lastWordIndex = -1;
164-
for (int i = 0; i < tokens.length; i++) {
165-
String cleanedToken = stripPunctuation(tokens[i]);
166-
if (!cleanedToken.isEmpty()) {
167-
if (firstWordIndex == -1) firstWordIndex = i;
168-
lastWordIndex = i;
169-
}
170-
}
171-
if (firstWordIndex == -1) return text;
172-
173-
StringBuilder result = new StringBuilder();
174-
for (int i = 0; i < tokens.length; i++) {
175-
String token = tokens[i];
176-
if (token.isBlank()) {
177-
result.append(token);
178-
continue;
179-
}
180-
String cleanedToken = stripPunctuation(token);
181-
if (cleanedToken.isEmpty()) {
182-
result.append(token);
183-
continue;
184-
}
185-
String lowercaseToken = cleanedToken.toLowerCase(Locale.ENGLISH);
186-
boolean isFirstWord = i == firstWordIndex;
187-
boolean isLastWord = i == lastWordIndex;
188-
189-
if (!isFirstWord && !isLastWord && TITLE_CASE_LOWERCASE_WORDS.contains(lowercaseToken)) {
190-
result.append(applyLowercaseToTokenPreservingPunctuation(token, lowercaseToken));
191-
} else if (hasInternalCapitalization(cleanedToken)) {
192-
result.append(token);
193-
} else {
194-
result.append(capitalizeFirstLetter(token));
195-
}
196-
}
197-
return result.toString();
198-
}
199-
200-
private static String applyLowercaseToTokenPreservingPunctuation(String token, String lowercaseWord) {
201-
var matcher = PUNCTUATION_WITH_WORD_PATTERN.matcher(token);
202-
return matcher.matches() ? matcher.group(1) + lowercaseWord + matcher.group(3) : token;
203-
}
204-
205-
private static String capitalizeFirstLetter(String token) {
206-
if (token.isEmpty() || SPECIAL_PREFIX_PATTERN.matcher(token).matches()) return token;
207-
var matcher = LEADING_PUNCTUATION_WITH_FIRST_LETTER_PATTERN.matcher(token);
208-
if (matcher.matches()) {
209-
return matcher.group(1) + Character.toUpperCase(matcher.group(2).charAt(0)) + matcher.group(3);
210-
}
211-
return token;
53+
return capitalization == Nls.Capitalization.Title
54+
? StringUtil.wordsToBeginFromUpperCase(string)
55+
: StringUtil.capitalize(StringUtil.wordsToBeginFromLowerCase(string));
21256
}
21357
}

jvm/jvm-analysis-java-tests/testSrc/com/intellij/codeInspection/NlsCapitalizationUtilTest.java

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import junit.framework.TestCase;
1919
import org.jetbrains.annotations.Nls;
2020

21-
import static com.intellij.codeInspection.NlsCapitalizationUtil.checkPunctuation;
2221
import static com.intellij.codeInspection.NlsCapitalizationUtil.isCapitalizationSatisfied;
2322

2423
public class NlsCapitalizationUtilTest extends TestCase {
@@ -118,6 +117,8 @@ public void testTitleCapitalizationPrepositions() {
118117
assertTitle("Search in Files");
119118
assertTitle("Go to Declaration");
120119
assertTitle("Copy from Here");
120+
assertTitle("Paste as Plain Text");
121+
assertTitle("Open In…");
121122
}
122123

123124
public void testTitleCapitalizationFirstAndLastWords() {
@@ -130,43 +131,11 @@ public void testTitleCapitalizationNotSatisfied() {
130131
assertNotCapitalization("Compare With The Latest Repository Version", Nls.Capitalization.Title);
131132
assertNotCapitalization("Search And Replace", Nls.Capitalization.Title);
132133
assertNotCapitalization("compare with Latest Version", Nls.Capitalization.Title);
134+
assertNotCapitalization("Compare With Latest Version", Nls.Capitalization.Title);
133135
assertNotCapitalization("Compare with", Nls.Capitalization.Title);
134-
}
135-
136-
public void testPunctuationSingleSentenceNoPeriod() {
137-
assertTrue("Single sentence should not have period", checkPunctuation("This is a single sentence"));
138-
}
139-
140-
public void testPunctuationSingleSentenceWithPeriod() {
141-
assertFalse("Single sentence should not end with period", checkPunctuation("This is a single sentence."));
142-
}
143-
144-
public void testPunctuationMultipleSentencesWithPeriods() {
145-
assertTrue("Multiple sentences should end with periods", checkPunctuation("First sentence. Second sentence."));
146-
}
147-
148-
public void testPunctuationMultipleSentencesWithoutFinalPeriod() {
149-
assertFalse("Multiple sentences should end with periods", checkPunctuation("First sentence. Second sentence"));
150-
}
151-
152-
public void testPunctuationNoDoubleQuotes() {
153-
assertFalse("Should not use double quotes", checkPunctuation("This is \"quoted\" text"));
154-
}
155-
156-
public void testPunctuationSingleQuotesAllowed() {
157-
assertTrue("Single quotes are allowed", checkPunctuation("This is 'quoted' text"));
158-
}
159-
160-
public void testPunctuationNoExclamation() {
161-
assertFalse("Should not use exclamation points", checkPunctuation("This is important!"));
162-
}
163-
164-
public void testPunctuationNoContractions() {
165-
assertFalse("Should not use contractions", checkPunctuation("Path can't be found"));
166-
}
167-
168-
public void testPunctuationDontAgainAllowed() {
169-
assertTrue("'Don't [verb] again' is allowed", checkPunctuation("Don't ask again"));
136+
assertNotCapitalization("Save Current Layout As New", Nls.Capitalization.Title);
137+
assertNotCapitalization("Compare with…", Nls.Capitalization.Title);
138+
assertNotCapitalization("Open in…", Nls.Capitalization.Title);
170139
}
171140

172141
public void testFixValueTitleWithArticles() {

platform/util/src/com/intellij/openapi/util/text/StringUtil.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -457,14 +457,22 @@ public static int difference(@NotNull String s1, @NotNull String s2) {
457457
// special string like ~java or _java; keep it as is
458458
continue;
459459
}
460-
if (!isPreposition(s, start, i - 1, ourOtherNonCapitalizableWords)) {
461-
boolean firstWord = start == 0 || isPunctuation(prevPrevChar);
462-
boolean lastWord = i >= length - 1|| isPunctuation(s.charAt(i + 1));
463-
if (!title || firstWord || lastWord || !isPreposition(s, start, i - 1, wordsToIgnore)) {
464-
if (buffer == null) {
465-
buffer = new StringBuilder(s);
466-
}
467-
buffer.setCharAt(start, title ? toUpperCase(currChar) : toLowerCase(currChar));
460+
461+
if (!isPreposition(s, start, i - 1, ourOtherNonCapitalizableWords)) { // keep unchanged special words
462+
if (buffer == null) {
463+
buffer = new StringBuilder(s);
464+
}
465+
466+
if (!title) {
467+
buffer.setCharAt(start, toLowerCase(currChar));
468+
}
469+
else {
470+
boolean firstWord = start == 0 || isPunctuation(prevPrevChar);
471+
boolean lastWord = i >= length - 1|| isPunctuation(s.charAt(i + 1));
472+
// Prepositions may occur in both upper/lower case in Title capitalization:
473+
// Example: Compare with the Latest Repository Version In…
474+
buffer.setCharAt(start, firstWord || lastWord || !isPreposition(s, start, i - 1, wordsToIgnore)
475+
? toUpperCase(currChar) : toLowerCase(currChar));
468476
}
469477
}
470478
}
@@ -475,7 +483,7 @@ public static int difference(@NotNull String s1, @NotNull String s2) {
475483
}
476484

477485
private static boolean isPunctuation(char c) {
478-
return c == '.' || c == '!' || c == ':' || c == '?';
486+
return c == '.' || c == '!' || c == ':' || c == '?' || c == '…';
479487
}
480488

481489
private static final String[] ourLowerCaseWords = {

plugins/devkit/intellij.devkit.i18n/testData/inspections/pluginXmlCapitalization/MyBundle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ group.BundleGroup.description=Bundle group description
1515
group.BundleGroupWrongCasing.text=group lower case text
1616
group.BundleGroupWrongCasing.description=group lower case description
1717

18-
action.OverrideBundleAction.ViaBundle.text=Action Text Override Via Bundle
18+
action.OverrideBundleAction.ViaBundle.text=Action Text Override via Bundle
1919
action.OverrideBundleAction.ViaBundleWrongCase.text=Action Text Override Via Bundle lower case
2020

2121
group.OverrideGroup.text=Group Text
22-
group.OverrideGroup.GroupViaBundle.text=Group Via Bundle Text
22+
group.OverrideGroup.GroupViaBundle.text=Group via Bundle Text
2323
group.OverrideGroup.GroupViaBundleWrongCase.text=override group wrong case
2424

2525
titleCaseKey=My Text

0 commit comments

Comments
 (0)