Skip to content

Conversation

@georgewrmarshall
Copy link
Contributor

@georgewrmarshall georgewrmarshall commented Nov 25, 2025

Description

This PR adds automated sentence case validation to the existing verify-locales script to enforce consistent capitalization across all English locale strings, as originally defined in PR #15285.

What is the reason for the change?

Since PR #15285 (Aug 2022), we've had content guidelines requiring sentence case for all UI strings. However, over time, new features have introduced title case violations (e.g., "Price Impact", "Network Menu", etc). There was no automated enforcement, so violations accumulated.

What is the improvement/solution?

1. Automated CI Check

  • Extended development/verify-locale-strings.js with sentence case validation
  • Violations are now detected automatically and fail CI
  • Blocks PRs with title case violations before merge

2. Special Cases Configuration

  • Created app/_locales/sentence-case-exceptions.json to define terms that should maintain title case:
    • Security terms: "Secret Recovery Phrase", "Private Key"
    • Branded features: "Snaps", "Transaction Shield", "Smart Account", "Smart Transactions", "Smart Swaps"
    • Product names: "MetaMask", "Ledger Live", etc.
    • Network names: "Ethereum Mainnet", etc.
    • Acronyms: NFT, API, RPC, etc.

3. Quoted Text Preservation

  • Preserves capitalization of text within quotes (UI element names)
  • Handles both single quotes: 'Get Signature'
  • And escaped double quotes: \"Switch to Smart Account\"
  • This ensures references to UI elements maintain their original capitalization

4. Auto-Fix Support

  • yarn verify-locales --fix automatically corrects all violations
  • Applied fix to correct 46 existing violations in English locale
  • en_GB auto-synced via existing script logic

Changelog

CHANGELOG entry: null

Related issues

Part of: https://consensyssoftware.atlassian.net/browse/MDP-428

Manual testing steps

  1. Run yarn verify-locales - should pass with no violations
  2. Introduce a test violation in app/_locales/en/messages.json:
    "testKey": {
      "message": "Some Title Case Text"
    }
  3. Run yarn verify-locales - should fail with:
    **en**: 1 sentence case violations
    Messages not following sentence case:
      - [ ] testKey: "Some Title Case Text" → "Some title case text"
    
  4. Run yarn verify-locales --fix - should auto-correct to sentence case
  5. Verify quoted text is preserved:
    "testQuoted": {
      "message": "Click on 'Some Button' to continue"
    }
    Should NOT be flagged (quotes preserve capitalization)
  6. Verify special cases are preserved (add test with "Smart Account" or "Snaps")

Screenshots/Recordings

Before

  • No automated enforcement
  • Title case violations could be merged without detection
  • Manual review required to catch violations

After

  • Automated CI check blocks violations
  • --fix flag provides easy correction
  • Clear reporting of violations with suggested fixes
  • Special cases properly preserved
after720.mov

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

@github-actions
Copy link
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-design-system All issues relating to design system in Extension label Nov 25, 2025
@georgewrmarshall georgewrmarshall changed the title chore: Add sentence case validation to verify-locales script chore: add sentence case validation to verify-locales script Nov 25, 2025
@georgewrmarshall georgewrmarshall self-assigned this Nov 25, 2025
@georgewrmarshall georgewrmarshall added the no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed label Nov 25, 2025
Copilot finished reviewing on behalf of georgewrmarshall November 25, 2025 00:27
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enforces consistent sentence case capitalization across all English locale strings by adding automated validation to the verify-locales script. The validation detects title case violations (e.g., "Price Impact" → "Price impact") and can automatically fix them with the --fix flag. The PR includes a configuration file for special case exceptions (branded terms, product names, security terminology), preserves quoted text capitalization, and corrects 46 existing violations in the English locale files.

Key Changes

  • Added sentence case validation logic with pattern detection and auto-fix support
  • Created exceptions configuration for branded/technical terms that should maintain title case
  • Fixed 46 capitalization violations across English locale files

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 9 comments.

File Description
development/verify-locale-strings.js Adds 170+ lines implementing sentence case validation with detection, conversion, and auto-fix logic
app/_locales/sentence-case-exceptions.json New configuration file defining 41 terms exempt from sentence case rules (brands, products, networks, acronyms)
app/_locales/en/messages.json Corrects 46 title case violations to sentence case
app/_locales/en_GB/messages.json Auto-synced with en locale corrections

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 503 to 511
if (sentenceCaseViolations.length > 0 && fix) {
const newLocale = { ...englishLocale };
for (const violation of sentenceCaseViolations) {
if (newLocale[violation.key]) {
newLocale[violation.key].message = violation.suggested;
}
}
await writeLocale('en', newLocale);
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both unusedMessages and sentenceCaseViolations exist and --fix is used, the locale file is written twice (lines 495-500, then 503-511). The second write will overwrite the first, losing the unused message deletions. The fixes should be combined into a single write operation. Merge both changes into one newLocale object before calling writeLocale() once.

Copilot uses AI. Check for mistakes.
"message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction."
},
"airDropPatternDescription": {
"message": "The token's on-chain history reveals prior instances of suspicious airdrop activities."
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing all sentence case violations using updated script yarn verify-locales:fix

},
"notificationItemCheckBlockExplorer": {
"message": "Check on the Block Explorer"
"message": "Check on the block explorer"
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this one. Is "Block Explorer" considered a special term @coreyjanssen?

},
"rewardsOnboardingStep4LegalDisclaimer2": {
"message": "Supplemental Terms of Use and Privacy Notice"
"message": "Supplemental terms of use and privacy notice"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should "Terms of Use" and "Privacy Notice" be special terms @coreyjanssen?

},
"betaTerms": {
"message": "Beta Terms of use"
"message": "Beta terms of use"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should "Terms of Use" be a special case @coreyjanssen?

"message": "Priority fee (aka “miner tip”) goes directly to miners and incentivizes them to prioritize your transaction."
},
"airDropPatternDescription": {
"message": "The token's on-chain history reveals prior instances of suspicious airdrop activities."
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to check this file matches app/_locales/en/messages.json as the yarn verify-locales:fix automates copying the changes.

@@ -0,0 +1,63 @@
{
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Special terms that are exempt and should remain Title Case or UPPERCASE

  - ~2,680 operations (one regex test per string)
  - ~50x faster performance improvement
@metamaskbot
Copy link
Collaborator

Builds ready [b1e70bc]
UI Startup Metrics (1235 ± 96 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard HomeuiStartup1235101114959612641449
load104586612598310841216
domContentLoaded103986112548210761206
domInteractive271599232297
firstPaint5009212303889901150
backgroundConnect2141992609219229
firstReactRender30196293546
getState311691123555
initialActions104112
loadScripts8316591048828641001
setupStore1152531221
numNetworkReqs1257820573
BrowserifyPower User HomeuiStartup19901548309228221002587
load998881180715110011392
domContentLoaded98587417941519761384
domInteractive34171923429121
firstPaint5918514283809501033
backgroundConnect22419728414230249
firstReactRender84421351895113
getState17113024827190226
initialActions107112
loadScripts77667315591497631181
setupStore21951102545
numNetworkReqs103652875995279
WebpackStandard HomeuiStartup8487101107898941033
load65456989879697836
domContentLoaded64956488978693829
domInteractive2816137232385
firstPaint246101894193216757
backgroundConnect1062951020
firstReactRender28205273339
getState271470113348
initialActions103112
loadScripts64656288177690819
setupStore1153341319
numNetworkReqs1257320571
WebpackPower User HomeuiStartup15731161301424816902013
load6225471236104616873
domContentLoaded6125401222103603868
domInteractive33161743328125
firstPaint309771236217547636
backgroundConnect1464681536
firstReactRender82431391691106
getState14912323415153181
initialActions103112
loadScripts6105381212102601860
setupStore19764132048
numNetworkReqs1606641575197369
FirefoxBrowserifyStandard HomeuiStartup12151065161510612701455
load101189512397010571136
domContentLoaded101089512397110571136
domInteractive55291402981118
firstPaint------
backgroundConnect321872103652
firstReactRender22184452135
getState1065161023
initialActions102012
loadScripts99187712176810331113
setupStore135138181031
numNetworkReqs1156515753
BrowserifyPower User HomeuiStartup25032025321726026842970
load1136932158614311511467
domContentLoaded1135931158614311511467
domInteractive1143351794112377
firstPaint------
backgroundConnect952640052107212
firstReactRender894315722100129
getState27260865217450709
initialActions218136
loadScripts1106915155714111151428
setupStore118877317289681
numNetworkReqs99613136478250
WebpackStandard HomeuiStartup14191285194011714531682
load1197106614318412551357
domContentLoaded1196106614308412551357
domInteractive4425153235984
firstPaint------
backgroundConnect4219131204492
firstReactRender27199182935
getState12675111242
initialActions2045422
loadScripts1172105013637812331335
setupStore135193201126
numNetworkReqs1156715653
WebpackPower User HomeuiStartup27492219370832629613346
load13591149185717113911771
domContentLoaded13591149185717113911771
domInteractive104315139699359
firstPaint------
backgroundConnect1032649262119203
firstReactRender82421972392116
getState27456901232421791
initialActions4056737
loadScripts13231112183416413531737
setupStore996662135101503
numNetworkReqs100532515880237
📊 Page Load Benchmark Results

Current Commit: b1e70bc | Date: 11/25/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.04s (±40ms) 🟡 | historical mean value: 1.03s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 724ms (±38ms) 🟢 | historical mean value: 718ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 77ms (±14ms) 🟢 | historical mean value: 77ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.04s 40ms 1.01s 1.35s 1.07s 1.35s
domContentLoaded 724ms 38ms 701ms 1.02s 757ms 1.02s
firstPaint 77ms 14ms 60ms 200ms 88ms 200ms
firstContentfulPaint 77ms 14ms 60ms 200ms 88ms 200ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.1 KiB (0.02%)
  • ui: 8.18 KiB (0.12%)
  • common: 128 Bytes (0%)

georgewrmarshall and others added 4 commits November 25, 2025 14:24
Implements 5 Copilot bot suggestions to improve code quality and fix bugs:

1. **Empty word guard**: Filter empty strings from word arrays to prevent false positives
   - Added `.filter(word => word.length > 0)` in hasTitleCaseViolation and convertToSentenceCase

2. **Performance optimization**: Pre-compiled regex for special case detection
   - Reduced complexity from O(n×m) to O(n) (~50x faster)
   - Built single regex at module load time instead of checking each exception per string

3. **Whitespace filtering**: Prevent edge cases with whitespace-only text
   - Filter empty strings after split to avoid `['']` arrays

4. **Overlapping terms fix** (CRITICAL BUG):
   - Sort special terms by position, then by length descending
   - Skip overlapping terms during processing
   - Prevents "MetaMask Portfolio" from being corrupted by overlapping "MetaMask" term

All changes tested and validated with test scripts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Addresses Copilot suggestion about placeholder replacement safety.

**Issue**: Using the same placeholder `___QUOTED___` for all quoted strings
with `replace()` in a loop only replaces the first occurrence. While this
worked sequentially (each replace() removes one placeholder, making the next
one "first"), it was fragile and not immediately obvious.

**Fix**:
- Use unique placeholders: `___QUOTED_0___`, `___QUOTED_1___`, etc.
- Store placeholder-text pairs in objects: `{placeholder, text}`
- Check if word contains placeholder to handle punctuation edge cases
  (e.g., `'Option A',` becomes `___QUOTED_0___,`)

**Testing**:
✅ All quoted strings correctly preserved
✅ Multiple quotes in sequence work correctly
✅ Mixed single and escaped double quotes handled
✅ Quoted text followed by punctuation preserved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@metamaskbot
Copy link
Collaborator

metamaskbot commented Nov 25, 2025

✨ Files requiring CODEOWNER review ✨

👨‍🔧 @MetaMask/core-extension-ux (3 files, +4 -4)
  • 📁 ui/
    • 📁 components/
      • 📁 multichain/
        • 📁 app-header/
          • 📁 __snapshots__/
            • 📄 app-header.test.js.snap +1 -1
        • 📁 disconnect-permissions-modal/
          • 📄 disconnect-permissions-modal.test.tsx +1 -1
        • 📁 pages/
          • 📁 gator-permissions/
            • 📁 components/
              • 📁 __snapshots__/
                • 📄 review-gator-permission-item.test.tsx.snap +2 -2

🧪 @MetaMask/qa (2 files, +3 -3)
  • 📁 test/
    • 📁 e2e/
      • 📁 page-objects/
        • 📁 pages/
          • 📁 settings/
            • 📄 settings-page.ts +2 -2
            • 📄 developer-options-page.ts +1 -1

🔐 @MetaMask/web3auth (1 files, +2 -2)
  • 📁 ui/
    • 📁 pages/
      • 📁 onboarding-flow/
        • 📁 create-password/
          • 📁 __snapshots__/
            • 📄 create-password.test.js.snap +2 -2

@metamaskbot
Copy link
Collaborator

Builds ready [056f18f]
UI Startup Metrics (1273 ± 102 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard HomeuiStartup12731057151810213211469
load107189112558911181232
domContentLoaded106588612468811131223
domInteractive28151422622104
firstPaint62087125342810801230
backgroundConnect22220426112227244
firstReactRender332096123952
getState341984124055
initialActions104112
loadScripts8506731033878911004
setupStore1272941323
numNetworkReqs1257820572
BrowserifyPower User HomeuiStartup20181546287328121792621
load97587314991409701356
domContentLoaded96286114891409521340
domInteractive36162194027153
firstPaint57414614953979391343
backgroundConnect21719625410221240
firstReactRender85451842092120
getState16512724224180217
initialActions104112
loadScripts76066712841397491131
setupStore21956112545
numNetworkReqs1416439477185369
WebpackStandard HomeuiStartup8176881106838531009
load64156186376679816
domContentLoaded63655785875674810
domInteractive2615106222293
firstPaint24796801178240698
backgroundConnect952741318
firstReactRender26204463139
getState261369133555
initialActions105112
loadScripts63355585073672800
setupStore1164161229
numNetworkReqs1257720573
WebpackPower User HomeuiStartup16851263251321718052048
load6645741197111653932
domContentLoaded6545661192112639927
domInteractive36172083530140
firstPaint289791061196367648
backgroundConnect1675291839
firstReactRender85451081595104
getState14712218614155176
initialActions102011
loadScripts6515641180110637917
setupStore21961142551
numNetworkReqs1567141479195391
FirefoxBrowserifyStandard HomeuiStartup13311098204814913921644
load109793213018311511238
domContentLoaded109692713018411491238
domInteractive70301533385135
firstPaint------
backgroundConnect54253154847170
firstReactRender24195772339
getState1168091024
initialActions102012
loadScripts106991412687811181212
setupStore126145151022
numNetworkReqs1256717664
BrowserifyPower User HomeuiStartup25001861325625426832905
load1123969154913411231480
domContentLoaded1122969154813511231479
domInteractive11834500101112383
firstPaint------
backgroundConnect942933349114179
firstReactRender86401812699141
getState25034889199288713
initialActions3131337
loadScripts1090948151712810941416
setupStore15212758187183625
numNetworkReqs1006127957106243
WebpackStandard HomeuiStartup14181222177912414661714
load1194104214839612471418
domContentLoaded1194104214829612471418
domInteractive53271843370125
firstPaint------
backgroundConnect4119114184381
firstReactRender26204252937
getState136124151144
initialActions102112
loadScripts1170102214399012271363
setupStore12680101232
numNetworkReqs1156816662
WebpackPower User HomeuiStartup26532047369629128243197
load13491131183918114301754
domContentLoaded13491131183918114301754
domInteractive10830492103101440
firstPaint------
backgroundConnect922743556109180
firstReactRender80391531887116
getState23265857207233793
initialActions4169837
loadScripts13151101180817913621725
setupStore1356721192135614
numNetworkReqs1006324756115243
📊 Page Load Benchmark Results

Current Commit: 056f18f | Date: 11/25/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.04s (±41ms) 🟡 | historical mean value: 1.03s ⬆️ (historical data)
  • domContentLoaded-> current mean value: 728ms (±38ms) 🟢 | historical mean value: 718ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 78ms (±11ms) 🟢 | historical mean value: 77ms ⬆️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.04s 41ms 1.02s 1.37s 1.07s 1.37s
domContentLoaded 728ms 38ms 703ms 1.02s 756ms 1.02s
firstPaint 78ms 11ms 64ms 180ms 88ms 180ms
firstContentfulPaint 78ms 11ms 64ms 180ms 88ms 180ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.1 KiB (0.02%)
  • ui: 9.95 KiB (0.14%)
  • common: 93 Bytes (0%)

georgewrmarshall and others added 2 commits November 25, 2025 15:21
Fixed all linting issues:

**require-unicode-regexp**: Added 'u' flag to all regex patterns
- Line 50: escapeRegex function
- Line 68: buildExceptionsRegex return
- Lines 84-85: hasTitleCaseViolation quote removal
- Line 89: hasTitleCaseViolation word split
- Lines 97, 100: Title case pattern checks
- Lines 190, 198: convertToSentenceCase quote replacement
- Line 206: convertToSentenceCase word split

**no-plusplus**: Replaced ++ with += 1
- Lines 193, 201: placeholderIndex increments

**no-shadow**: Fixed variable shadowing
- Line 227: Renamed destructured 'text' to 'quotedText' to avoid
  shadowing the outer 'text' parameter

All changes verified - script still passes validation tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Comment on lines +56 to +57
"Third Party Service",
"Third Party Services"
Copy link
Contributor Author

@georgewrmarshall georgewrmarshall Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should "Third Party Service/s" even be a special case? 🤔 cc @coreyjanssen

Copilot finished reviewing on behalf of georgewrmarshall November 25, 2025 02:25
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 46 to 262
function buildExceptionsRegex(exceptions) {
const patterns = [];

// Escape special regex characters for exact matches
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');

// Add exact matches (escaped to treat as literals)
exceptions.exactMatches.forEach((term) => {
patterns.push(escapeRegex(term));
});

// Add acronyms (escaped to treat as literals)
exceptions.acronyms.forEach((acronym) => {
patterns.push(escapeRegex(acronym));
});

// Add existing regex patterns (already in regex format)
Object.values(exceptions.patterns).forEach((pattern) => {
patterns.push(pattern);
});

// Combine all patterns with OR operator
return new RegExp(patterns.join('|'), 'u');
}

// Pre-compile the exceptions regex once at module load time
const specialCaseRegex = buildExceptionsRegex(sentenceCaseExceptions);

// Helper function to check if text contains special case terms
// Now uses pre-compiled regex for O(n) instead of O(n*m) performance
function containsSpecialCase(text) {
return specialCaseRegex.test(text);
}

// Helper function to detect title case violations
function hasTitleCaseViolation(text) {
// Remove quoted text (single quotes and escaped double quotes) before checking
// Quoted text refers to UI elements and should preserve capitalization
let textWithoutQuotes = text.replace(/'[^']*'/gu, ''); // Remove 'text'
textWithoutQuotes = textWithoutQuotes.replace(/\\"[^"]*\\"/gu, ''); // Remove \"text\"

// Ignore single words (filter out empty strings from whitespace)
const words = textWithoutQuotes
.split(/\s+/u)
.filter((word) => word.length > 0);
if (words.length < 2) {
return false;
}

// Check if multiple words start with capital letters (Title Case pattern)
// This pattern: "Word Word" or "Word Word Word"
const titleCasePattern = /^([A-Z][a-z]+\s+)+[A-Z][a-z]+/u;

// Also catch patterns like "In Progress", "Not Available"
const multipleCapsPattern = /\b[A-Z][a-z]+\s+[A-Z][a-z]+\b/u;

return (
titleCasePattern.test(textWithoutQuotes) ||
multipleCapsPattern.test(textWithoutQuotes)
);
}

// Helper function to convert to sentence case while preserving special cases
function toSentenceCase(text) {
// If text contains special cases, we need to be careful
if (containsSpecialCase(text)) {
// Build a map of special terms and their positions
const specialTerms = [];

// Find all special terms from exact matches
for (const term of sentenceCaseExceptions.exactMatches) {
let index = text.indexOf(term);
while (index !== -1) {
specialTerms.push({ term, start: index, end: index + term.length });
index = text.indexOf(term, index + 1);
}
}

// Find all acronyms
for (const acronym of sentenceCaseExceptions.acronyms) {
let index = text.indexOf(acronym);
while (index !== -1) {
specialTerms.push({
term: acronym,
start: index,
end: index + acronym.length,
});
index = text.indexOf(acronym, index + 1);
}
}

// Sort by position first, then by length descending (prefer longer matches)
// This ensures overlapping terms like "MetaMask Portfolio" are processed before "MetaMask"
specialTerms.sort((a, b) => {
if (a.start !== b.start) {
return a.start - b.start;
}
// If same start position, prefer longer match
return b.end - b.start - (a.end - a.start);
});

// Build result preserving special terms
let result = '';
let lastIndex = 0;

for (const special of specialTerms) {
// Skip overlapping terms (already covered by a previous term)
if (special.start < lastIndex) {
continue;
}

// Process text before this special term
const before = text.substring(lastIndex, special.start);
if (before) {
result += convertToSentenceCase(before);
}
// Add the special term as-is
result += special.term;
lastIndex = special.end;
}

// Process remaining text
if (lastIndex < text.length) {
result += convertToSentenceCase(text.substring(lastIndex));
}

return result;
}

return convertToSentenceCase(text);
}

// Simple sentence case conversion
function convertToSentenceCase(text) {
if (!text) {
return text;
}

// Extract quoted text (single quotes and escaped double quotes) and preserve them
const quotedTexts = [];
let textToProcess = text;
let placeholderIndex = 0;

// Find all single-quoted text and replace with unique placeholders
textToProcess = textToProcess.replace(/'([^']*)'/gu, (match) => {
const uniquePlaceholder = `___QUOTED_${placeholderIndex}___`;
quotedTexts.push({ placeholder: uniquePlaceholder, text: match });
placeholderIndex += 1;
return uniquePlaceholder;
});

// Find all escaped double-quoted text and replace with unique placeholders
textToProcess = textToProcess.replace(/\\"([^"]*)\\"/gu, (match) => {
const uniquePlaceholder = `___QUOTED_${placeholderIndex}___`;
quotedTexts.push({ placeholder: uniquePlaceholder, text: match });
placeholderIndex += 1;
return uniquePlaceholder;
});

// Convert to sentence case
const words = textToProcess.split(/\s+/u).filter((word) => word.length > 0);
let converted = words
.map((word, index) => {
// Check if word contains a placeholder (exact match or with punctuation)
if (
quotedTexts.some(
(q) => word === q.placeholder || word.includes(q.placeholder),
)
) {
return word;
}
if (index === 0) {
// First word: capitalize first letter
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
// Other words: all lowercase
return word.toLowerCase();
})
.join(' ');

// Restore quoted text with unique placeholders
quotedTexts.forEach(({ placeholder, text: quotedText }) => {
converted = converted.replace(placeholder, quotedText);
});

return converted;
}

// Validate sentence case compliance for a locale
function validateSentenceCaseCompliance(locale) {
const violations = [];

for (const [key, value] of Object.entries(locale)) {
if (!value || !value.message) {
continue;
}

const text = value.message;

// Skip if contains special cases
if (containsSpecialCase(text)) {
continue;
}

// Check for title case violations
if (hasTitleCaseViolation(text)) {
const suggested = toSentenceCase(text);
violations.push({
key,
current: text,
suggested,
});
}
}

return violations;
}
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new functions added for sentence case validation lack JSDoc documentation. Consider adding JSDoc comments to improve code maintainability:

/**
 * Builds a compiled regex pattern from sentence case exceptions for efficient matching.
 * @param {Object} exceptions - The exceptions object containing exactMatches, acronyms, and patterns
 * @param {string[]} exceptions.exactMatches - Array of exact terms to match
 * @param {string[]} exceptions.acronyms - Array of acronyms to match
 * @param {Object} exceptions.patterns - Object containing regex patterns
 * @returns {RegExp} Compiled regex pattern matching all exceptions
 */
function buildExceptionsRegex(exceptions) { ... }

/**
 * Checks if text contains any special case terms that should be exempt from sentence case rules.
 * @param {string} text - The text to check
 * @returns {boolean} True if text contains special case terms
 */
function containsSpecialCase(text) { ... }

/**
 * Detects if text violates sentence case by using title case capitalization.
 * Ignores quoted text and single words.
 * @param {string} text - The text to check
 * @returns {boolean} True if text has title case violations
 */
function hasTitleCaseViolation(text) { ... }

/**
 * Converts text to sentence case while preserving special case terms.
 * @param {string} text - The text to convert
 * @returns {string} Text converted to sentence case
 */
function toSentenceCase(text) { ... }

/**
 * Simple sentence case conversion without special case preservation.
 * @param {string} text - The text to convert
 * @returns {string} Text converted to sentence case
 */
function convertToSentenceCase(text) { ... }

/**
 * Validates that all locale messages follow sentence case conventions.
 * @param {Object} locale - The locale object containing message keys and values
 * @returns {Array<{key: string, current: string, suggested: string}>} Array of violations
 */
function validateSentenceCaseCompliance(locale) { ... }

Copilot uses AI. Check for mistakes.
Comment on lines +189 to +203
// Find all single-quoted text and replace with unique placeholders
textToProcess = textToProcess.replace(/'([^']*)'/gu, (match) => {
const uniquePlaceholder = `___QUOTED_${placeholderIndex}___`;
quotedTexts.push({ placeholder: uniquePlaceholder, text: match });
placeholderIndex += 1;
return uniquePlaceholder;
});

// Find all escaped double-quoted text and replace with unique placeholders
textToProcess = textToProcess.replace(/\\"([^"]*)\\"/gu, (match) => {
const uniquePlaceholder = `___QUOTED_${placeholderIndex}___`;
quotedTexts.push({ placeholder: uniquePlaceholder, text: match });
placeholderIndex += 1;
return uniquePlaceholder;
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could simplify this logic by unifying the format in app/_locales/en/messages.json but not sure if there is a reason to have both?

Comment on lines +524 to +534
if (sentenceCaseViolations.length) {
console.log(
`**en**: ${sentenceCaseViolations.length} sentence case violations`,
);
log.info(`Messages not following sentence case:`);
sentenceCaseViolations.forEach(function (violation) {
log.info(
` - [ ] ${violation.key}: "${violation.current}" → "${violation.suggested}"`,
);
});
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't log any other violations so I'm not sure if we should log the sentence case ones but it might be worth it for the time being so it's clear to engineers that there has been an update made to the verify-locale script 🤔

@metamaskbot
Copy link
Collaborator

Builds ready [4a35cc0]
UI Startup Metrics (1230 ± 100 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard HomeuiStartup12301030150310012841420
load103685912779110821205
domContentLoaded103085412708910751197
domInteractive2515114202185
firstPaint48980129340610091166
backgroundConnect21319332916214239
firstReactRender27195492947
getState301665103649
initialActions105112
loadScripts824658107087870971
setupStore1052331119
numNetworkReqs1257820571
BrowserifyPower User HomeuiStartup19711605274227920732580
load100588615091319981392
domContentLoaded99187914951329831384
domInteractive34161623229133
firstPaint53812915074079661369
backgroundConnect22620026312231252
firstReactRender89461442097130
getState16513122922184200
initialActions107112
loadScripts78167312461317651165
setupStore22996142648
numNetworkReqs101642845794273
WebpackStandard HomeuiStartup828704109679866998
load64856584577711805
domContentLoaded64356083677706800
domInteractive2615113222189
firstPaint21681842168190681
backgroundConnect952941018
firstReactRender26194263037
getState261355123750
initialActions103112
loadScripts64055882675704790
setupStore1052841116
numNetworkReqs1257620571
WebpackPower User HomeuiStartup16671233237122418062066
load6695801239112662950
domContentLoaded6595731231112649944
domInteractive37172543928134
firstPaint271931073184252656
backgroundConnect1674881734
firstReactRender86471201697106
getState14812322016156179
initialActions104112
loadScripts6565711222110647935
setupStore22959143250
numNetworkReqs1557139573196333
FirefoxBrowserifyStandard HomeuiStartup12891101179213013411603
load106893513628111111225
domContentLoaded106793013618111111225
domInteractive62312573783133
firstPaint------
backgroundConnect43221762941113
firstReactRender22184152236
getState126186191021
initialActions102012
loadScripts104391913437810881193
setupStore12593131033
numNetworkReqs1156716663
BrowserifyPower User HomeuiStartup24642002338328126212983
load1097927158414311091429
domContentLoaded1096921158414311081429
domInteractive11931687115114390
firstPaint------
backgroundConnect922736558106250
firstReactRender88431662298133
getState25835814211323773
initialActions207126
loadScripts1065908153213910711409
setupStore1396778179132624
numNetworkReqs100613086378244
WebpackStandard HomeuiStartup15421330206812915781797
load1300113615699013421477
domContentLoaded1300113615699013421477
domInteractive782814032101131
firstPaint------
backgroundConnect51182583550129
firstReactRender302182103242
getState1174751319
initialActions103123
loadScripts1267111815418013091432
setupStore166146211171
numNetworkReqs1256816664
WebpackPower User HomeuiStartup26912174344528929103192
load13481150185017513751763
domContentLoaded13481149184917513751763
domInteractive1062746396102364
firstPaint------
backgroundConnect1062647977113255
firstReactRender83412062293117
getState27157872221429783
initialActions506612341
loadScripts13121098181616913161719
setupStore1627757218185723
numNetworkReqs100612535879243
📊 Page Load Benchmark Results

Current Commit: 4a35cc0 | Date: 11/25/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.03s (±39ms) 🟡 | historical mean value: 1.03s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 715ms (±35ms) 🟢 | historical mean value: 718ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 76ms (±13ms) 🟢 | historical mean value: 77ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.03s 39ms 1.00s 1.33s 1.05s 1.33s
domContentLoaded 715ms 35ms 696ms 988ms 734ms 988ms
firstPaint 76ms 13ms 60ms 192ms 84ms 192ms
firstContentfulPaint 76ms 13ms 60ms 192ms 84ms 192ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.1 KiB (0.02%)
  • ui: 9.95 KiB (0.14%)
  • common: 93 Bytes (0%)

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@metamaskbot
Copy link
Collaborator

Builds ready [fe91217]
UI Startup Metrics (1214 ± 111 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard HomeuiStartup1214991175311112491417
load103183714819610621202
domContentLoaded102583414719510541195
domInteractive2514143222087
firstPaint55092123540710271188
backgroundConnect21119225511215235
firstReactRender27195073144
getState31166193349
initialActions103112
loadScripts821643126194847980
setupStore1174761222
numNetworkReqs1257820573
BrowserifyPower User HomeuiStartup22091679295830624162858
load1089913151713010981428
domContentLoaded1073898151112810761421
domInteractive39192233737145
firstPaint66884151942210551381
backgroundConnect25222133420261293
firstReactRender1004620530111164
getState18613530334203252
initialActions104112
loadScripts84169412961298431203
setupStore241192132851
numNetworkReqs1206436866129275
WebpackStandard HomeuiStartup8306961121858661005
load65356883678697810
domContentLoaded64856382778691805
domInteractive2615109212391
firstPaint23887804167233687
backgroundConnect953651019
firstReactRender27197593142
getState24134993144
initialActions104112
loadScripts64556181776689797
setupStore1043041217
numNetworkReqs1257720573
WebpackPower User HomeuiStartup17041280232624219072072
load6876021068100688969
domContentLoaded6765941063100673950
domInteractive37181683136121
firstPaint286851051201292715
backgroundConnect1685691836
firstReactRender85471222098111
getState15312920416158186
initialActions104112
loadScripts673592105398671941
setupStore221057142654
numNetworkReqs1507040373195334
FirefoxBrowserifyStandard HomeuiStartup1168105716189512071363
load97989511806310121120
domContentLoaded97789011806410121120
domInteractive4628202274595
firstPaint------
backgroundConnect3121111133352
firstReactRender22174562234
getState10613213921
initialActions102012
loadScripts9598801155629921099
setupStore1157491323
numNetworkReqs1156714750
BrowserifyPower User HomeuiStartup25262035340332527473159
load1135928182217811401530
domContentLoaded1134928182117811401530
domInteractive12932856135111456
firstPaint------
backgroundConnect1002848175118235
firstReactRender86411622598134
getState26726841208415735
initialActions208127
loadScripts1099909178217110921511
setupStore1296762170117645
numNetworkReqs100623046180245
WebpackStandard HomeuiStartup15021300201613315241797
load1266110915309113091466
domContentLoaded1265110915299113091466
domInteractive76311893598132
firstPaint------
backgroundConnect45182333146107
firstReactRender312273113065
getState116135131120
initialActions102022
loadScripts1240109315048312841413
setupStore157205221436
numNetworkReqs1256717663
WebpackPower User HomeuiStartup27322201340330230043316
load13711131181418414121777
domContentLoaded13701131181418414111776
domInteractive10832500102101436
firstPaint------
backgroundConnect1042865072128207
firstReactRender84421602095115
getState27575854219439757
initialActions4072938
loadScripts13391111176717813881736
setupStore122871316999616
numNetworkReqs99562466077237
📊 Page Load Benchmark Results

Current Commit: fe91217 | Date: 11/25/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.03s (±36ms) 🟡 | historical mean value: 1.03s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 718ms (±34ms) 🟢 | historical mean value: 718ms ⬇️ (historical data)
  • firstContentfulPaint-> current mean value: 76ms (±14ms) 🟢 | historical mean value: 77ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.03s 36ms 1.00s 1.31s 1.05s 1.31s
domContentLoaded 718ms 34ms 694ms 977ms 739ms 977ms
firstPaint 76ms 14ms 60ms 196ms 88ms 196ms
firstContentfulPaint 76ms 14ms 60ms 196ms 88ms 196ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.1 KiB (0.02%)
  • ui: 9.95 KiB (0.14%)
  • common: 93 Bytes (0%)

georgewrmarshall and others added 2 commits November 25, 2025 20:42
**Problem**: When both unusedMessages and sentenceCaseViolations exist
and --fix is used, the locale file was written twice. The second write
would overwrite the first, losing the unused message deletions.

**Root cause**:
- Line 544-549: First write deleted unused messages
- Line 552-559: Second write applied sentence case fixes but used
  original englishLocale as base, not the already-fixed version
- Result: Unused message deletions were lost

**Solution**:
Merged both fixes into a single operation:
1. Create one newLocale object from englishLocale
2. Apply unused message deletions to newLocale
3. Apply sentence case fixes to newLocale
4. Write locale file once with all changes applied

This ensures both types of fixes are preserved in the final output.

Addresses GitHub Copilot suggestion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@metamaskbot
Copy link
Collaborator

Builds ready [0a7579d]
UI Startup Metrics (1239 ± 104 ms)
PlatformBuildTypePageMetricMean (ms)Min (ms)Max (ms)Std Dev (ms)P 75 (ms)P 95 (ms)
ChromeBrowserifyStandard HomeuiStartup12391030143810413311421
load105586412619411421195
domContentLoaded104986112569311351190
domInteractive2615126242192
firstPaint51882119141310341176
backgroundConnect21019126510213227
firstReactRender29195393248
getState31186593451
initialActions103112
loadScripts847659105892941990
setupStore1173031214
numNetworkReqs1257320571
BrowserifyPower User HomeuiStartup19211533323929619792599
load98487119081539721355
domContentLoaded97086518981539531350
domInteractive34172073229122
firstPaint5189013813579261028
backgroundConnect22120235318228245
firstReactRender84471481993121
getState16412624929184225
initialActions103112
loadScripts76566315551447481141
setupStore22989122549
numNetworkReqs102642895796270
WebpackStandard HomeuiStartup8316981024798651012
load64856285077695801
domContentLoaded64355884276690797
domInteractive2615115212188
firstPaint22073800152221614
backgroundConnect1063661126
firstReactRender26204963140
getState251459103446
initialActions102111
loadScripts64055583375688795
setupStore1062531116
numNetworkReqs1257720573
WebpackPower User HomeuiStartup16731253247625718862093
load6745871257108677938
domContentLoaded6645811252109665932
domInteractive37181843333136
firstPaint277107933172264674
backgroundConnect1684771736
firstReactRender88471231597106
getState15112620416159180
initialActions104112
loadScripts6615791242107663923
setupStore23966152755
numNetworkReqs1527340774199335
FirefoxBrowserifyStandard HomeuiStartup12421044155511613361461
load104189112679011051199
domContentLoaded104089112679011051197
domInteractive59291623283128
firstPaint------
backgroundConnect4022141224299
firstReactRender22175062235
getState11697111022
initialActions103012
loadScripts101687612418310781161
setupStore1155791024
numNetworkReqs1156815655
BrowserifyPower User HomeuiStartup24361879316826425712980
load1094917158414310991451
domContentLoaded1093910158314310991450
domInteractive1123345394110376
firstPaint------
backgroundConnect892331045108174
firstReactRender84401552395132
getState25161871214295779
initialActions3115237
loadScripts1065899153713910701412
setupStore15811717197174642
numNetworkReqs1005931556114238
WebpackStandard HomeuiStartup15191338206112615571810
load1277111315449013351463
domContentLoaded1277110815378913351463
domInteractive75281923990167
firstPaint------
backgroundConnect46211812651106
firstReactRender29217783239
getState137154161337
initialActions103122
loadScripts1249109714838613091440
setupStore147187191138
numNetworkReqs1156415655
WebpackPower User HomeuiStartup26962056373032628463365
load13391091180417713941735
domContentLoaded13381091180417713941735
domInteractive102284088996346
firstPaint------
backgroundConnect1032769783120207
firstReactRender81421591991114
getState29670897244471809
initialActions3041437
loadScripts13011072174216613301681
setupStore92673314180528
numNetworkReqs996324853116238
📊 Page Load Benchmark Results

Current Commit: 0a7579d | Date: 11/25/2025

📄 Localhost MetaMask Test Dapp

Samples: 100

Summary

  • pageLoadTime-> current mean value: 1.03s (±69ms) 🟡 | historical mean value: 1.04s ⬇️ (historical data)
  • domContentLoaded-> current mean value: 722ms (±65ms) 🟢 | historical mean value: 721ms ⬆️ (historical data)
  • firstContentfulPaint-> current mean value: 78ms (±45ms) 🟢 | historical mean value: 81ms ⬇️ (historical data)

📈 Detailed Results

Metric Mean Std Dev Min Max P95 P99
pageLoadTime 1.03s 69ms 1.00s 1.66s 1.07s 1.66s
domContentLoaded 722ms 65ms 695ms 1.32s 757ms 1.32s
firstPaint 78ms 45ms 56ms 524ms 84ms 524ms
firstContentfulPaint 78ms 45ms 56ms 524ms 84ms 524ms
largestContentfulPaint 0ms 0ms 0ms 0ms 0ms 0ms
Bundle size diffs [🚨 Warning! Bundle size has increased!]
  • background: 1.1 KiB (0.02%)
  • ui: 10.66 KiB (0.15%)
  • common: 505 Bytes (0.01%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-changelog no-changelog Indicates no external facing user changes, therefore no changelog documentation needed size-L team-design-system All issues relating to design system in Extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants