Skip to content

Commit 0ae2ee9

Browse files
committed
Quality: shared pluralize helper for log/error/UI strings
Hand-rolled `format!("{n} files")` reads as `1 files` when `n == 1`. New `pluralize(n, "file")` returns `"1 file"` / `"3 files"`; pass an explicit plural for irregulars (`pluralize_with(n, "entry", "entries")` in Rust, `pluralize(n, 'entry', 'entries')` in TS). - New `apps/desktop/src-tauri/src/pluralize.rs` (generic over `usize`/`u32`/`u64`/…). - New `apps/desktop/src/lib/utils/pluralize.ts` with the third arg optional (defaults to `+s`). - Old `column_meta::pluralize` and `selection-info-utils::pluralize` re-export from the new locations. - Swept ~50 real sites across indexing, search, MCP, AI, viewer, transfer dialogs, license dialog, error-report dialog, etc. User-facing strings (license dialog, error-report sample headings, debug toast count) included. New `pluralize-noun` check (`scripts/check/checks/pluralize-noun.go`) catches new `{var} <plural>` patterns in Rust + TS/Svelte. Filters comments, structured `key=value` log shapes, non-plural English words (`was`, `is`, `exceeds`, …), Svelte attribute bindings, and the git `stash@{n}` shorthand. Opt-out via `// allowed-pluralize-noun: <reason>`. Runs in ~0.3 s.
1 parent e597d24 commit 0ae2ee9

49 files changed

Lines changed: 721 additions & 125 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/src-tauri/src/ai/api_keys.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! key sits in the OS-native secret backend (macOS Keychain, Linux Secret Service, etc.) instead
55
//! of `settings.json`. One entry per provider keyed as `ai.apiKey.<providerId>`.
66
7+
use crate::pluralize::pluralize;
78
use crate::secrets::SecretStoreError;
89
use log::{debug, info};
910
use serde::{Deserialize, Serialize};
@@ -51,7 +52,10 @@ pub fn save(provider_id: &str, api_key: &str) -> Result<(), AiApiKeyError> {
5152
let key = store_key(provider_id);
5253
let key_len = api_key.len();
5354
crate::secrets::store().set(&key, api_key.as_bytes())?;
54-
info!("AI API key saved for provider {provider_id} ({key_len} bytes)");
55+
info!(
56+
"AI API key saved for provider {provider_id} ({})",
57+
pluralize(key_len, "byte")
58+
);
5559
Ok(())
5660
}
5761

apps/desktop/src-tauri/src/ai/client_local_llama_test.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ async fn local_streaming_emits_multiple_chunks_progressively() {
6969
.await
7070
.expect("stream should open against local server");
7171

72-
let mut chunks = 0;
72+
let mut chunks: usize = 0;
7373
let mut total = String::new();
7474
let mut first_chunk_at = None;
7575
while let Some(item) = stream.next().await {
@@ -95,7 +95,8 @@ async fn local_streaming_emits_multiple_chunks_progressively() {
9595

9696
log::info!(
9797
target: "ai_smoke",
98-
"local stream → {chunks} chunks, ttft={ttft:?}, total={total_elapsed:?}, content: {total}"
98+
"local stream → {}, ttft={ttft:?}, total={total_elapsed:?}, content: {total}",
99+
crate::pluralize::pluralize(chunks, "chunk")
99100
);
100101
}
101102

apps/desktop/src-tauri/src/ai/client_real_anthropic_test.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async fn smoke_claude_haiku_stream() {
7575
.expect("stream open");
7676

7777
let mut text = String::new();
78-
let mut chunks = 0;
78+
let mut chunks: usize = 0;
7979
while let Some(item) = stream.next().await {
8080
let chunk = item.expect("chunk ok");
8181
text.push_str(&chunk);
@@ -84,5 +84,9 @@ async fn smoke_claude_haiku_stream() {
8484

8585
assert!(!text.trim().is_empty(), "expected non-empty assembled text");
8686
assert!(chunks > 0, "expected at least one chunk");
87-
log::info!(target: "ai_smoke", "claude-3-5-haiku stream → {chunks} chunks, total: {text}");
87+
log::info!(
88+
target: "ai_smoke",
89+
"claude-3-5-haiku stream → {}, total: {text}",
90+
crate::pluralize::pluralize(chunks, "chunk")
91+
);
8892
}

apps/desktop/src-tauri/src/ai/client_real_openai_test.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,17 @@ async fn collect_stream(backend: &AiBackend, model_label: &str) -> String {
113113
.await
114114
.expect("stream open");
115115
let mut text = String::new();
116-
let mut chunks = 0;
116+
let mut chunks: usize = 0;
117117
while let Some(item) = stream.next().await {
118118
let chunk = item.expect("chunk ok");
119119
text.push_str(&chunk);
120120
chunks += 1;
121121
}
122-
log::info!(target: "ai_smoke", "{model_label} stream → {chunks} chunks, total: {text}");
122+
log::info!(
123+
target: "ai_smoke",
124+
"{model_label} stream → {}, total: {text}",
125+
crate::pluralize::pluralize(chunks, "chunk")
126+
);
123127
text
124128
}
125129

apps/desktop/src-tauri/src/ai/extract.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::fs;
44
use std::path::Path;
55
use tauri::{AppHandle, Manager, Runtime};
66

7+
use crate::pluralize::pluralize;
8+
79
/// Binary filename for the llama-server executable.
810
pub const LLAMA_SERVER_BINARY: &str = "llama-server";
911

@@ -30,7 +32,7 @@ pub fn extract_bundled_llama_server<R: Runtime>(app: &AppHandle<R>, ai_dir: &Pat
3032

3133
let entries = fs::read_dir(&resource_dir).map_err(|e| format!("Failed to read bundled AI directory: {e}"))?;
3234

33-
let mut copied_count = 0;
35+
let mut copied_count: usize = 0;
3436
for entry in entries {
3537
let entry = entry.map_err(|e| format!("Failed to read directory entry: {e}"))?;
3638
let src_path = entry.path();
@@ -83,6 +85,6 @@ pub fn extract_bundled_llama_server<R: Runtime>(app: &AppHandle<R>, ai_dir: &Pat
8385
return Err(String::from("llama-server binary not found in bundled resources"));
8486
}
8587

86-
log::debug!("AI: copied {copied_count} files from bundled resources");
88+
log::debug!("AI: copied {} from bundled resources", pluralize(copied_count, "file"));
8789
Ok(())
8890
}

apps/desktop/src-tauri/src/ai/manager.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use super::process::{
1313
};
1414
use super::{AiState, AiStatus, ModelInfo, get_default_model, get_model_by_id, is_local_ai_supported};
1515
use crate::ignore_poison::IgnorePoison;
16+
use crate::pluralize::pluralize;
1617
use std::collections::HashMap;
1718
use std::fs;
1819
use std::path::{Path, PathBuf};
@@ -992,7 +993,10 @@ async fn wait_for_server_health(ai_dir: &Path, pid: u32, port: u16) -> Result<()
992993
if i % 10 == 9 {
993994
log::debug!("AI server: still waiting for health check ({}s)...", (i + 1) / 2);
994995
if let Some((line_count, last_line)) = log_diagnostics(ai_dir) {
995-
log::debug!("AI server: log has {line_count} lines, last: {last_line}");
996+
log::debug!(
997+
"AI server: log has {}, last: {last_line}",
998+
pluralize(line_count, "line")
999+
);
9961000
}
9971001
}
9981002
}

apps/desktop/src-tauri/src/commands/error_reporter.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use crate::error_reporter::{
77
self, BundleKind, BundleManifest, BundleScope, FLOW_A_BUNDLE_CAP_MB, settings_defaults::SettingValue,
88
};
9+
use crate::pluralize::pluralize;
910
use serde::Serialize;
1011
use std::collections::HashMap;
1112

@@ -124,8 +125,9 @@ pub async fn save_error_report_to_disk(app: tauri::AppHandle, user_note: Option<
124125
fn validate_user_note(user_note: Option<String>) -> Result<Option<String>, String> {
125126
match user_note {
126127
Some(n) if n.chars().count() > MAX_USER_NOTE_CHARS => Err(format!(
127-
"User note is too long ({} chars). Maximum is {MAX_USER_NOTE_CHARS} chars.",
128-
n.chars().count(),
128+
"User note is too long ({}). Maximum is {}.",
129+
pluralize(n.chars().count(), "char"),
130+
pluralize(MAX_USER_NOTE_CHARS, "char"),
129131
)),
130132
other => Ok(other),
131133
}

apps/desktop/src-tauri/src/commands/search.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::search::{
1818
};
1919

2020
use crate::indexing::writer::WRITER_GENERATION;
21+
use crate::pluralize::pluralize_with;
2122
use crate::search::ai::{self, query_builder as ai_query_builder};
2223

2324
#[derive(Debug, Clone, Serialize, specta::Type)]
@@ -106,7 +107,10 @@ pub async fn prepare_search_index(app: tauri::AppHandle) -> Result<PrepareResult
106107
backstop_timer: Some(backstop),
107108
load_cancel: Some(cancel),
108109
});
109-
log::debug!("Search index ready: {entry_count} entries");
110+
log::debug!(
111+
"Search index ready: {}",
112+
pluralize_with(entry_count, "entry", "entries")
113+
);
110114
// Emit event to frontend
111115
use tauri::Emitter;
112116
let _ = app_clone.emit(

apps/desktop/src-tauri/src/file_system/git/column_meta.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,7 @@ use gix::object::tree::EntryKind;
2525
use super::friendly::{FriendlyGitError, FriendlyGitErrorKind};
2626
use super::repo::{RepoHandle, count_ahead_behind};
2727

28-
/// Pluralizes a noun based on count: `1 commit` / `12 commits`.
29-
pub fn pluralize(count: u64, singular: &str, plural: &str) -> String {
30-
if count == 1 {
31-
format!("1 {}", singular)
32-
} else {
33-
format!("{} {}", count, plural)
34-
}
35-
}
28+
pub use crate::pluralize::{pluralize, pluralize_with};
3629

3730
/// Picks a fallback comparison branch for ahead/behind when the branch has
3831
/// no configured upstream. Tries `main`, then `master`. Returns the
@@ -311,9 +304,9 @@ mod tests {
311304

312305
#[test]
313306
fn pluralize_singular_and_plural() {
314-
assert_eq!(pluralize(0, "file", "files"), "0 files");
315-
assert_eq!(pluralize(1, "file", "files"), "1 file");
316-
assert_eq!(pluralize(2, "file", "files"), "2 files");
317-
assert_eq!(pluralize(12, "branch", "branches"), "12 branches");
307+
assert_eq!(pluralize(0_u64, "file"), "0 files");
308+
assert_eq!(pluralize(1_u64, "file"), "1 file");
309+
assert_eq!(pluralize(2_u64, "file"), "2 files");
310+
assert_eq!(pluralize_with(12_u64, "branch", "branches"), "12 branches");
318311
}
319312
}

apps/desktop/src-tauri/src/file_system/git/log.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,10 +207,10 @@ pub fn list_commits(handle: &RepoHandle, repo_root: &Path) -> Result<Vec<FileEnt
207207
// count, still a useful "size of this snapshot" cue.
208208
if let Some(n) = files_changed_count(&repo, id) {
209209
fe.size = Some(n);
210-
fe.display_size = Some(column_meta::pluralize(n, "file", "files"));
210+
fe.display_size = Some(column_meta::pluralize(n, "file"));
211211
fe.display_size_tooltip = Some(format!(
212212
"{} changed compared to the parent commit",
213-
column_meta::pluralize(n, "file", "files")
213+
column_meta::pluralize(n, "file")
214214
));
215215
}
216216
out.push(fe);

0 commit comments

Comments
 (0)