1use std::collections::BTreeMap;
39use std::sync::OnceLock;
40
41use gpui::{App, BorrowAppContext, Global, SharedString};
42
43macro_rules! string_keys {
46 ($( $variant:ident => $name:literal, $default:literal ; )*) => {
47 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
53 #[non_exhaustive]
54 pub enum StringKey {
55 $( #[doc = $default] $variant, )*
56 }
57
58 impl StringKey {
59 pub const ALL: &'static [StringKey] = &[ $( StringKey::$variant, )* ];
61
62 pub const fn name(self) -> &'static str {
67 match self {
68 $( StringKey::$variant => $name, )*
69 }
70 }
71
72 pub const fn english(self) -> &'static str {
74 match self {
75 $( StringKey::$variant => $default, )*
76 }
77 }
78
79 pub fn from_name(name: &str) -> Option<Self> {
81 StringKey::ALL.iter().copied().find(|key| key.name() == name)
82 }
83 }
84 };
85}
86
87string_keys! {
88 Copy => "common.copy", "Copy";
91 Dismiss => "common.dismiss", "Dismiss";
92 TryAgain => "common.try-again", "Try again";
93 Loading => "common.loading", "Loading";
94 MoreActions => "common.more-actions", "More actions";
95 Expand => "common.expand", "Expand";
96 Collapse => "common.collapse", "Collapse";
97
98 PasswordReveal => "password.reveal", "Reveal password";
100 PasswordConceal => "password.conceal", "Conceal password";
101
102 CalendarNoMonth => "calendar.no-month", "No month to show";
104 CalendarPreviousMonth => "calendar.previous-month", "Previous month";
105 CalendarNextMonth => "calendar.next-month", "Next month";
106 CalendarUnknownMonth => "calendar.unknown-month", "This calendar does not know which month to show";
107 CalendarUnknownMonthDetail => "calendar.unknown-month-detail", "Nothing is selected, the host has not said what day it is, and no month was given.";
108
109 DateInputOpen => "date-input.open", "Open the calendar";
111 DateInputPlaceholder => "date-input.placeholder", "Date";
112
113 RangeUnset => "range.unset", "No range chosen yet.";
115 RangeIncomplete => "range.incomplete", "{0} to an end that has not been chosen yet.";
116 RangeComplete => "range.complete", "{0} to {1}.";
117 RangeInverted => "range.inverted", "The end, {0}, comes before the start, {1}.";
118 RangeUncheckable => "range.uncheckable", "The host cannot list the days in this range, so none of them were checked.";
119 RangeBlockedDay => "range.blocked-day", "{0}: {1}";
120
121 TimeHour => "time.hour", "Hour";
123 TimeMinute => "time.minute", "Minute";
124 TimeSecond => "time.second", "Second";
125 TimeMeridiem => "time.meridiem", "Half of the day";
126
127 DockCollapseRegion => "dock.collapse-region", "Collapse region";
129 SplitResizeHandle => "split.resize-handle", "Resize panes";
130 StatusStale => "status.stale", "stale";
131 ScrollbarVertical => "scrollbar.vertical", "Vertical";
132 ScrollbarHorizontal => "scrollbar.horizontal", "Horizontal";
133
134 BreadcrumbHiddenOne => "breadcrumb.hidden-one", "1 hidden level";
136 BreadcrumbHiddenMany => "breadcrumb.hidden-many", "{0} hidden levels";
137
138 AnchorMoreSections => "anchor.more-sections", "More sections";
140
141 PaginationFirst => "pagination.first", "First page";
143 PaginationPrevious => "pagination.previous", "Previous page";
144 PaginationNext => "pagination.next", "Next page";
145 PaginationLast => "pagination.last", "Last page";
146 PaginationMorePages => "pagination.more-pages", "{0} more pages";
147 PaginationPageOfTotal => "pagination.page-of-total", "Page {0} of {1}";
148 PaginationPage => "pagination.page", "Page {0}";
149
150 WizardBack => "wizard.back", "Back";
152 WizardNext => "wizard.next", "Next";
153 WizardFinish => "wizard.finish", "Finish";
154 WizardReturnsTo => "wizard.returns-to", "Returns to";
155
156 ImageViewerNotSupplied => "image-viewer.not-supplied", "Not supplied — {0}";
158 ImageViewerPrevious => "image-viewer.previous", "Previous image";
159 ImageViewerNext => "image-viewer.next", "Next image";
160 ImageViewerContain => "image-viewer.contain", "Contain";
161 ImageViewerCover => "image-viewer.cover", "Cover";
162 ImageViewerSizeUnknown => "image-viewer.size-unknown", "Size unknown";
163 ImageViewerEmpty => "image-viewer.empty", "No images";
164
165 MarkdownImageAlt => "markdown.image-alt", "Image";
167 MarkdownImageNotFetched => "markdown.image-not-fetched", "Not fetched — {0}";
168 MarkdownPlainText => "markdown.plain-text", "plain text";
169 MarkdownTask => "markdown.task", "Task";
170 MarkdownUnrenderedHtml => "markdown.unrendered-html", "unrendered html";
171 MarkdownShowMoreOne => "markdown.show-more-one", "Show 1 more line";
172 MarkdownShowMoreMany => "markdown.show-more-many", "Show {0} more lines";
173
174 MessageSending => "message.sending", "Sending";
176 MessageSent => "message.sent", "Sent";
177 MessageDelivered => "message.delivered", "Delivered";
178 MessageRead => "message.read", "Read";
179 MessageStreaming => "message.streaming", "Streaming";
180 MessageMoreOne => "message.more-one", "1 more message";
181 MessageMoreMany => "message.more-many", "{0} more messages";
182 MessageNewOne => "message.new-one", "1 new message";
183 MessageNewMany => "message.new-many", "{0} new messages";
184 MessageShowMoreOne => "message.show-more-one", "1 more line";
185 MessageShowMoreMany => "message.show-more-many", "{0} more lines";
186 TimeUnknown => "time.unknown", "Time unknown";
187
188 TransportBuffered => "transport.buffered", "Buffered";
190 TransportTimeUnknown => "transport.time-unknown", "Time unknown";
191 TransportDurationUnknown => "transport.duration-unknown", "Duration unknown";
192 TransportPosition => "transport.position", "Playback position";
193 TransportPlay => "transport.play", "Play";
194 TransportPause => "transport.pause", "Pause";
195 TransportPlaying => "transport.playing", "Playing";
196 TransportPaused => "transport.paused", "Paused";
197 TransportBuffering => "transport.buffering", "Waiting for data";
198 TransportMute => "transport.mute", "Mute";
199 TransportUnmute => "transport.unmute", "Unmute";
200 TransportVolume => "transport.volume", "Volume";
201 TransportPreviousTrack => "transport.previous-track", "Previous track";
202 TransportNextTrack => "transport.next-track", "Next track";
203
204 MediaFixture => "media.fixture", "Fixture";
206 MediaNoTransport => "media.no-transport", "No player";
207 MediaNoTransportDetail => "media.no-transport-detail", "No player is connected to this surface, so there is nothing to start.";
208 MediaNoBackend => "media.no-backend", "No playback backend";
209 MediaFailed => "media.failed", "This could not be played";
210 MediaEmpty => "media.empty", "Nothing loaded";
211 MediaWaveform => "media.waveform", "Waveform";
212 VideoNoFrames => "video.no-frames", "No picture";
213 VideoNoFramesDetail => "video.no-frames-detail", "The transport holds this video and no frames have been supplied for it.";
214
215 ModelEmpty => "model.empty", "No model";
217 ModelEmptyDetail => "model.empty-detail", "Nothing has been handed to this viewer.";
218 ModelRefused => "model.refused", "This model was refused";
219 ModelTooLarge => "model.too-large", "Too many {0}: it asks for {1}, and the limit is {2}.";
220 ModelRejected => "model.rejected", "It is outside the subset this reader accepts ({0}).";
221 ModelFlat => "model.flat", "Flat";
222 ModelWireframe => "model.wireframe", "Wireframe";
223 ModelReset => "model.reset", "Reset the view";
224 ModelCount => "model.count", "{0} {1}";
225 ModelMeshes => "model.meshes", "Meshes";
226 ModelVertices => "model.vertices", "Vertices";
227 ModelTriangles => "model.triangles", "Triangles";
228
229 SelectPlaceholder => "select.placeholder", "Select";
231 ComboboxNoMatch => "combobox.no-match", "Nothing here answers “{0}”";
232 ComboboxCreateHint => "combobox.create-hint", "Press enter to add it as a new value.";
233 ComboboxClosedHint => "combobox.closed-hint", "This field only accepts one of the options offered.";
234
235 CascaderPlaceholder => "cascader.placeholder", "Select";
237 CascaderUnstarted => "cascader.unstarted", "Nothing has been asked for yet";
238 CascaderEmpty => "cascader.empty", "No options";
239 CascaderUnavailable => "cascader.unavailable", "Options unavailable";
240 CascaderError => "cascader.error", "Could not load options";
241
242 DropzoneRefusal => "dropzone.refusal", "This zone does not take that.";
244
245 FilterBarLabel => "filter-bar.label", "Filters";
247 FilterBarAdd => "filter-bar.add", "Add filter";
248 FilterBarClear => "filter-bar.clear", "Clear all";
249 FilterBarCounting => "filter-bar.counting", "Counting…";
250 FilterBarResultsNoun => "filter-bar.results-noun", "results";
251
252 InlineEditPlaceholder => "inline-edit.placeholder", "Empty";
254 KeybindingUnbound => "keybinding.unbound", "Not bound";
255 KeybindingPrompt => "keybinding.prompt", "Press a shortcut";
256 KeymapAdd => "keymap.add", "Add binding";
257 KeymapRemove => "keymap.remove", "Remove";
258 KeymapReset => "keymap.reset", "Reset to defaults";
259 KeymapEffective => "keymap.effective", "Current bindings";
260 KeymapDefaults => "keymap.defaults", "Defaults";
261 KeymapResultCount => "keymap.result-count", "{0} commands";
262
263 NumberDecrease => "number.decrease", "Decrease";
265 NumberIncrease => "number.increase", "Increase";
266 NumberNotANumber => "number.not-a-number", "This is not a number.";
267 NumberBelowMinimum => "number.below-minimum", "The smallest accepted value is {0}.";
268 NumberAboveMaximum => "number.above-maximum", "The largest accepted value is {0}.";
269
270 SettingsManagedBy => "settings.managed-by", "Managed by {0}";
272 SettingsInapplicable => "settings.inapplicable", "Not available here";
273
274 TagInputPlaceholder => "tag-input.placeholder", "Add";
276 TagInputDuplicate => "tag-input.duplicate", "“{0}” is already here";
277 TagInputFull => "tag-input.full", "This field holds at most {0}; “{1}” was not added";
278 TagInputUsed => "tag-input.used", "{0} of {1} used";
279 TagRemove => "tag.remove", "Remove {0}";
280
281 DescriptionUnknown => "description.unknown", "Unknown";
283 DescriptionNotApplicable => "description.not-applicable", "Not applicable";
284 DescriptionCopy => "description.copy", "Copy {0}";
285 DescriptionCharacters => "description.characters", "{0} characters";
286
287 CountOfTotal => "common.count-of-total", "{0} of {1}";
291
292 PalettePlaceholder => "palette.placeholder", "Type a command";
294 PaletteNoMatch => "palette.no-match", "No command matches “{0}”";
295 PaletteEmptyDetail => "palette.empty-detail", "Every command this application was given is listed here.";
296
297 KbdSuper => "kbd.super", "Win";
300 KbdControl => "kbd.control", "Ctrl";
301 KbdAlt => "kbd.alt", "Alt";
302 KbdShift => "kbd.shift", "Shift";
303
304 GridSelectedNoun => "grid.selected-noun", "selected";
306 GridSelectAllLoaded => "grid.select-all-loaded", "Select all loaded rows";
307 GridSelectAllTotal => "grid.select-all-total", "Select all {0}";
308 GridClearSelection => "grid.clear-selection", "Clear selection";
309 GridResizeColumn => "grid.resize-column", "Resize {0}";
310 GridLoadingRows => "grid.loading-rows", "Loading rows";
311 GridLoadFailed => "grid.load-failed", "Could not load rows";
312 GridEmpty => "grid.empty", "No rows";
313
314 DiagnosticsUnstarted => "diagnostics.unstarted", "Diagnostics have not been requested";
316 DiagnosticsEmpty => "diagnostics.empty", "No diagnostics";
317 DiagnosticsNoMatch => "diagnostics.no-match", "No diagnostics match these filters";
318 DiagnosticsUnavailable => "diagnostics.unavailable", "Diagnostics unavailable";
319 DiagnosticsError => "diagnostics.error", "Could not load diagnostics";
320 DiagnosticsFilterField => "diagnostics.filter-field", "Severity";
321 DiagnosticsFilterOperator => "diagnostics.filter-operator", "is";
322 DiagnosticsSeverityError => "diagnostics.severity-error", "Error";
323 DiagnosticsSeverityWarning => "diagnostics.severity-warning", "Warning";
324 DiagnosticsSeverityInformation => "diagnostics.severity-information", "Information";
325 DiagnosticsSeverityHint => "diagnostics.severity-hint", "Hint";
326
327 DragFileOne => "drag.file-one", "1 file";
329 DragFileMany => "drag.file-many", "{0} files";
330
331 CopyDone => "copy.done", "Copied";
336 CopyFailed => "copy.failed", "Not copied";
337 CopyFailedDetail => "copy.failed-detail", "The clipboard did not take it.";
338
339 ApprovalDecline => "approval.decline", "Decline";
342 ApprovalApproveOnce => "approval.approve-once", "Approve once";
343 ApprovalAlwaysSession => "approval.always-session", "Always for this session";
344 ApprovalAlwaysTool => "approval.always-tool", "Always for {0}";
345 ApprovalAlwaysPath => "approval.always-path", "Always in {0}";
346 ApprovalAlwaysHost => "approval.always-host", "Always on {0}";
347 ApprovalPending => "approval.pending", "Waiting for your answer";
348 ApprovalDeclined => "approval.declined", "Declined";
349 ApprovalApproved => "approval.approved", "Approved: {0}";
350 ApprovalOnceScope => "approval.once-scope", "this time only";
351 ApprovalExpired => "approval.expired", "This request expired before it was answered";
352 ApprovalSuperseded => "approval.superseded", "Replaced by {0}";
353
354 PermissionAllowed => "permission.allowed", "Allowed";
356 PermissionDenied => "permission.denied", "Denied";
357 PermissionAsk => "permission.ask", "Ask every time";
358 PermissionNotApplicable => "permission.not-applicable", "Does not apply";
359 PermissionSubjectHeading => "permission.subject-heading", "Subject";
360 PermissionInherited => "permission.inherited", "Inherited from {0}";
361 PermissionSetHere => "permission.set-here", "Set here";
362 PermissionCellName => "permission.cell-name", "{0}: {1}";
363
364 CostMeasured => "cost.measured", "{0}";
367 CostEstimated => "cost.estimated", "{0} (estimated)";
368 CostEstimateMark => "cost.estimate-mark", "Estimate";
369 CostUnavailable => "cost.unavailable", "Unavailable";
370 CostLastVerified => "cost.last-verified", "Last verified {0}";
371 ContextUnknownLimit => "context.unknown-limit", "Limit unknown";
372 JsonWithheld => "json.withheld", "withheld";
376 JsonRootValue => "json.root-value", "Value";
377 JsonShapeEntries => "json.shape-entries", "{0} entries";
378 JsonShapeItems => "json.shape-items", "{0} items";
379 JsonShapeValue => "json.shape-value", "a value";
380
381 SchemaUnrenderable => "schema.unrenderable", "This field cannot be shown here, so it has to be filled in some other way.";
383 SchemaUnrenderableRequired => "schema.unrenderable-required", "This field is required and cannot be shown here, so this form cannot complete the call.";
384 SchemaNoChoices => "schema.no-choices", "No choices were offered, so there is nothing to pick.";
385 SchemaRequiredMissing => "schema.required-missing", "This field is required.";
386 SchemaUnrenderableOne => "schema.unrenderable-one", "1 field cannot be shown here.";
387 SchemaUnrenderableMany => "schema.unrenderable-many", "{0} fields cannot be shown here.";
388
389 ServerConnected => "server.connected", "Connected";
391 ServerConnecting => "server.connecting", "Connecting";
392 ServerDisconnected => "server.disconnected", "Disconnected";
393 ServerFailed => "server.failed", "Failed";
394 ServerDisabled => "server.disabled", "Turned off";
395 ServerTools => "server.tools", "Tools";
396 ServerSkills => "server.skills", "Skills";
397 ServerResources => "server.resources", "Resources";
398 ServerOfferingsUnasked => "server.offerings-unasked", "Nothing has been asked for yet";
399 ServerOfferingsUnaskedDetail => "server.offerings-unasked-detail", "This connection has not been asked what it offers.";
400 ServerOfferingsAsking => "server.offerings-asking", "Asking what this connection offers";
401 ServerOfferingsNone => "server.offerings-none", "This connection offers nothing";
402 ServerOfferingsNoneDetail => "server.offerings-none-detail", "It answered, and the answer was empty.";
403 ServerOfferingsUnavailable => "server.offerings-unavailable", "What this connection offers is unknown";
404 ServerEmpty => "server.empty", "No connections";
405 ServerEmptyDetail => "server.empty-detail", "Nothing has been connected yet.";
406
407 AgentArguments => "agent.arguments", "Arguments";
412 AgentResult => "agent.result", "Result";
413 AgentPendingApproval => "agent.pending-approval", "Waiting for approval";
414 AgentRunning => "agent.running", "Running";
415 AgentSucceeded => "agent.succeeded", "Succeeded";
416 AgentFailed => "agent.failed", "Failed";
417 AgentDeclined => "agent.declined", "Declined";
418 AgentPending => "agent.pending", "Pending";
419 AgentDone => "agent.done", "Done";
420 AgentSkipped => "agent.skipped", "Skipped";
421 AgentNoOutput => "agent.no-output", "This tool returned nothing";
422 AgentElapsedUnknown => "agent.elapsed-unknown", "Elapsed time unknown";
423 AgentTruncated => "agent.truncated", "{0} of {1} lines shown";
424 AgentLinesOne => "agent.lines-one", "1 line";
425 AgentLinesMany => "agent.lines-many", "{0} lines";
426 AgentStepsDoneOne => "agent.steps-done-one", "1 step done";
427 AgentStepsDoneMany => "agent.steps-done-many", "{0} steps done";
428 AgentReasoning => "agent.reasoning", "Reasoning";
429 AgentReasoningWithheld => "agent.reasoning-withheld", "Withheld";
430 AgentReasoningThinking => "agent.reasoning-thinking", "Thinking";
431 AgentReasoningAbsent => "agent.reasoning-absent", "No reasoning was returned";
432
433 TabDirty => "tab.dirty", "Unsaved changes";
436 TabSaving => "tab.saving", "Saving";
437 TabSaveFailed => "tab.save-failed", "Could not save";
438 TabClose => "tab.close", "Close {0}";
439 TabMoreTabs => "tab.more-tabs", "More tabs";
440
441 SearchPlaceholder => "search.placeholder", "Find";
443 SearchNoHits => "search.no-hits", "No results";
444 SearchCounting => "search.counting", "Counting…";
445 SearchNotSearched => "search.not-searched", "Nothing searched yet";
446 SearchTooMany => "search.too-many", "More than {0}";
447 SearchHitOne => "search.hit-one", "1 result";
448 SearchHitMany => "search.hit-many", "{0} results";
449 SearchNext => "search.next", "Next result";
450 SearchPrevious => "search.previous", "Previous result";
451 SearchCaseSensitive => "search.case-sensitive", "Match case";
452 SearchWholeWord => "search.whole-word", "Whole word";
453 ReplacePlaceholder => "replace.placeholder", "Replace with";
454 ReplaceOne => "replace.one", "Replace";
455 ReplaceAllCounted => "replace.all-counted", "Replace all {0}";
456 ReplaceAllUncounted => "replace.all-uncounted", "Replace all";
457 ReplaceAllUncountable => "replace.all-uncountable", "Nobody has counted the results yet, so this cannot say how many it would change.";
458
459 NotificationsTitle => "notifications.title", "Notifications";
461 NotificationsEmpty => "notifications.empty", "Nothing to report";
462 NotificationsEmptyDetail => "notifications.empty-detail", "Notifications that have come and gone are kept here.";
463 NotificationsClearAll => "notifications.clear-all", "Clear all";
464 NotificationsMarkAllRead => "notifications.mark-all-read", "Mark all as read";
465 NotificationsUnread => "notifications.unread", "Unread";
466 NotificationsUnreadCount => "notifications.unread-count", "{0} unread";
467 NotificationsAtLeast => "notifications.at-least", "{0}+";
468
469 FailureTitle => "failure.title", "This panel could not be shown";
471 FailureAttempts => "failure.attempts", "Tried {0} times";
472 FailureRetrying => "failure.retrying", "Trying again";
473
474 CodeLineNumbers => "code.line-numbers", "Line numbers";
476 CodeLineAdded => "code.line-added", "Added";
477 CodeLineRemoved => "code.line-removed", "Removed";
478 CodeLineChanged => "code.line-changed", "Changed";
479 CodeLineHighlighted => "code.line-highlighted", "Highlighted";
480 CodeLineError => "code.line-error", "Error";
481 CodeEmpty => "code.empty", "Nothing to show";
482
483 LogFollow => "log.follow", "Follow output";
486 LogPause => "log.pause", "Pause output";
487 LogFollowing => "log.following", "Following newest";
488 LogPaused => "log.paused", "Follow paused";
489 LogEmpty => "log.empty", "No log entries";
490 LogUnavailable => "log.unavailable", "Log unavailable";
491 LogError => "log.error", "Could not load log";
492 DiffFile => "diff.file", "File";
493 DiffHunk => "diff.hunk", "Hunk";
494 DiffContextLine => "diff.context-line", "Context line";
495 DiffChangedLine => "diff.changed-line", "Changed line";
496 DiffEmpty => "diff.empty", "No differences";
497 SparklineEmpty => "sparkline.empty", "No readings";
498 SparklineUnavailable => "sparkline.unavailable", "Reading unavailable";
499 SparklineError => "sparkline.error", "Could not load reading";
500 SparklineCurrent => "sparkline.current", "Current: {0}";
501 SparklineMinimum => "sparkline.minimum", "Minimum: {0}";
502 SparklineMaximum => "sparkline.maximum", "Maximum: {0}";
503 SparklineRange => "sparkline.range", "Minimum {0}; maximum {1}";
504
505 UploadQueued => "upload.queued", "Queued";
507 UploadUploading => "upload.uploading", "Uploading";
508 UploadDone => "upload.done", "Uploaded";
509 UploadFailed => "upload.failed", "Failed";
510 UploadCancelled => "upload.cancelled", "Cancelled";
511 UploadRefused => "upload.refused", "Not accepted";
512 UploadCancel => "upload.cancel", "Cancel {0}";
513 UploadRemove => "upload.remove", "Remove {0}";
514 UploadOverall => "upload.overall", "Uploading";
515 UploadEmpty => "upload.empty", "No files yet";
516
517 BrowserPanel => "browser.panel", "Browser";
521 BrowserBack => "browser.back", "Back";
522 BrowserForward => "browser.forward", "Forward";
523 BrowserReload => "browser.reload", "Reload";
524 BrowserNoAddress => "browser.no-address", "No address";
525 BrowserEmpty => "browser.empty", "No page content";
526 BrowserEmptyDetail => "browser.empty-detail", "The page loaded without content.";
527 BrowserUnavailable => "browser.unavailable", "Web content unavailable";
528 BrowserNoEngineDetail => "browser.no-engine-detail", "This build cannot display web content.";
529 BrowserError => "browser.error", "Could not load";
530 BrowserNoViewport => "browser.no-viewport", "No page surface";
531 BrowserNoViewportDetail => "browser.no-viewport-detail",
532 "The host reported a ready page but supplied no viewport.";
533
534 OfferingCatalogEmpty => "offering-catalog.empty", "No offerings";
536 OfferingCatalogNoMatch => "offering-catalog.no-match", "No matching offerings";
537 OfferingSourceLoading => "offering-source.loading", "{0}: Loading offerings";
538 OfferingSourceEmpty => "offering-source.empty", "{0}: No offerings";
539 OfferingSourceUnavailable => "offering-source.unavailable", "{0}: Offerings unavailable";
540 OfferingSourceError => "offering-source.error", "{0}: Could not load offerings";
541 OfferingSourceStale => "offering-source.stale", "{0}: Showing last verified offerings";
542}
543
544#[derive(Debug, Clone, Default)]
550pub struct Strings {
551 overrides: BTreeMap<StringKey, SharedString>,
552}
553
554impl Global for Strings {}
555
556impl Strings {
557 pub fn new() -> Self {
559 Self::default()
560 }
561
562 pub fn set(&mut self, key: StringKey, text: impl Into<SharedString>) -> &mut Self {
564 self.overrides.insert(key, text.into());
565 self
566 }
567
568 pub fn clear(&mut self, key: StringKey) -> &mut Self {
570 self.overrides.remove(&key);
571 self
572 }
573
574 pub fn clear_all(&mut self) -> &mut Self {
576 self.overrides.clear();
577 self
578 }
579
580 pub fn extend(
582 &mut self,
583 entries: impl IntoIterator<Item = (StringKey, SharedString)>,
584 ) -> &mut Self {
585 self.overrides.extend(entries);
586 self
587 }
588
589 pub fn is_overridden(&self, key: StringKey) -> bool {
591 self.overrides.contains_key(&key)
592 }
593
594 pub fn text(&self, key: StringKey) -> SharedString {
596 match self.overrides.get(&key) {
597 Some(text) => text.clone(),
598 None => SharedString::new_static(key.english()),
599 }
600 }
601
602 pub fn format(&self, key: StringKey, args: &[&str]) -> SharedString {
608 SharedString::from(interpolate(self.text(key).as_ref(), args))
609 }
610}
611
612fn interpolate(template: &str, args: &[&str]) -> String {
613 let mut out = String::with_capacity(template.len());
614 let mut rest = template;
615 while let Some(open) = rest.find('{') {
616 let (before, tail) = rest.split_at(open);
617 out.push_str(before);
618 let Some(close) = tail.find('}') else {
619 out.push_str(tail);
620 return out;
621 };
622 let slot = &tail[1..close];
623 match slot.parse::<usize>().ok().and_then(|index| args.get(index)) {
624 Some(value) => out.push_str(value),
625 None => out.push_str(&tail[..=close]),
626 }
627 rest = &tail[close + 1..];
628 }
629 out.push_str(rest);
630 out
631}
632
633pub trait ActiveStrings {
636 fn strings(&self) -> &Strings;
637}
638
639impl ActiveStrings for App {
640 fn strings(&self) -> &Strings {
644 static ENGLISH: OnceLock<Strings> = OnceLock::new();
645 self.try_global::<Strings>()
646 .unwrap_or_else(|| ENGLISH.get_or_init(Strings::new))
647 }
648}
649
650pub fn install(cx: &mut App) {
653 if !cx.has_global::<Strings>() {
654 cx.set_global(Strings::new());
655 }
656}
657
658pub fn set_strings(entries: impl IntoIterator<Item = (StringKey, SharedString)>, cx: &mut App) {
661 install(cx);
662 cx.update_global::<Strings, ()>(|strings, _| {
663 strings.extend(entries);
664 });
665 cx.refresh_windows();
666}
667
668pub fn reset_strings(cx: &mut App) {
670 install(cx);
671 cx.update_global::<Strings, ()>(|strings, _| {
672 strings.clear_all();
673 });
674 cx.refresh_windows();
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[test]
682 fn every_key_has_a_unique_name_and_english() {
683 let mut names: Vec<&str> = StringKey::ALL.iter().map(|key| key.name()).collect();
684 names.sort_unstable();
685 let before = names.len();
686 names.dedup();
687 assert_eq!(names.len(), before, "two keys share a name");
688 for key in StringKey::ALL {
689 assert!(!key.english().is_empty(), "{} has no English", key.name());
690 assert_eq!(StringKey::from_name(key.name()), Some(*key));
691 }
692 }
693
694 #[test]
695 fn an_empty_catalogue_answers_in_english() {
696 let strings = Strings::new();
697 assert_eq!(strings.text(StringKey::Copy), "Copy");
698 assert_eq!(strings.text(StringKey::TryAgain), "Try again");
699 }
700
701 #[test]
702 fn an_override_replaces_only_what_it_names() {
703 let mut strings = Strings::new();
704 strings.set(StringKey::Copy, "Kopieren");
705 assert_eq!(strings.text(StringKey::Copy), "Kopieren");
706 assert_eq!(strings.text(StringKey::TryAgain), "Try again");
707 strings.clear(StringKey::Copy);
708 assert_eq!(strings.text(StringKey::Copy), "Copy");
709 }
710
711 #[test]
712 fn a_template_takes_its_arguments_in_any_order() {
713 let mut strings = Strings::new();
714 assert_eq!(
715 strings.format(StringKey::RangeComplete, &["Monday", "Friday"]),
716 "Monday to Friday."
717 );
718 strings.set(StringKey::RangeComplete, "{1} back to {0}.");
719 assert_eq!(
720 strings.format(StringKey::RangeComplete, &["Monday", "Friday"]),
721 "Friday back to Monday."
722 );
723 }
724
725 #[test]
726 fn a_placeholder_with_no_argument_stays_visible() {
727 let strings = Strings::new();
728 assert_eq!(
729 strings.format(StringKey::RangeComplete, &["Monday"]),
730 "Monday to {1}."
731 );
732 }
733
734 #[test]
735 fn every_placeholder_in_the_english_is_numbered_from_zero() {
736 for key in StringKey::ALL {
737 let english = key.english();
738 let mut expected = 0usize;
739 let mut rest = english;
740 while let Some(open) = rest.find('{') {
741 let tail = &rest[open..];
742 let close = tail.find('}').unwrap_or_else(|| {
743 panic!("{} has an unclosed placeholder", key.name());
744 });
745 let slot = &tail[1..close];
746 assert_eq!(
747 slot.parse::<usize>().ok(),
748 Some(expected),
749 "{} numbers its placeholders out of order",
750 key.name()
751 );
752 expected += 1;
753 rest = &tail[close + 1..];
754 }
755 }
756 }
757}