use std::collections::BTreeMap;
use std::sync::OnceLock;
use gpui::{App, BorrowAppContext, Global, SharedString};
macro_rules! string_keys {
($( $variant:ident => $name:literal, $default:literal ; )*) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum StringKey {
$( #[doc = $default] $variant, )*
}
impl StringKey {
pub const ALL: &'static [StringKey] = &[ $( StringKey::$variant, )* ];
pub const fn name(self) -> &'static str {
match self {
$( StringKey::$variant => $name, )*
}
}
pub const fn english(self) -> &'static str {
match self {
$( StringKey::$variant => $default, )*
}
}
pub fn from_name(name: &str) -> Option<Self> {
StringKey::ALL.iter().copied().find(|key| key.name() == name)
}
}
};
}
string_keys! {
Copy => "common.copy", "Copy";
Dismiss => "common.dismiss", "Dismiss";
TryAgain => "common.try-again", "Try again";
Loading => "common.loading", "Loading";
MoreActions => "common.more-actions", "More actions";
Expand => "common.expand", "Expand";
Collapse => "common.collapse", "Collapse";
PasswordReveal => "password.reveal", "Reveal password";
PasswordConceal => "password.conceal", "Conceal password";
CalendarNoMonth => "calendar.no-month", "No month to show";
CalendarPreviousMonth => "calendar.previous-month", "Previous month";
CalendarNextMonth => "calendar.next-month", "Next month";
CalendarUnknownMonth => "calendar.unknown-month", "This calendar does not know which month to show";
CalendarUnknownMonthDetail => "calendar.unknown-month-detail", "Nothing is selected, the host has not said what day it is, and no month was given.";
DateInputOpen => "date-input.open", "Open the calendar";
DateInputPlaceholder => "date-input.placeholder", "Date";
RangeUnset => "range.unset", "No range chosen yet.";
RangeIncomplete => "range.incomplete", "{0} to an end that has not been chosen yet.";
RangeComplete => "range.complete", "{0} to {1}.";
RangeInverted => "range.inverted", "The end, {0}, comes before the start, {1}.";
RangeUncheckable => "range.uncheckable", "The host cannot list the days in this range, so none of them were checked.";
RangeBlockedDay => "range.blocked-day", "{0}: {1}";
TimeHour => "time.hour", "Hour";
TimeMinute => "time.minute", "Minute";
TimeSecond => "time.second", "Second";
TimeMeridiem => "time.meridiem", "Half of the day";
DockCollapseRegion => "dock.collapse-region", "Collapse region";
SplitResizeHandle => "split.resize-handle", "Resize panes";
StatusStale => "status.stale", "stale";
ScrollbarVertical => "scrollbar.vertical", "Vertical";
ScrollbarHorizontal => "scrollbar.horizontal", "Horizontal";
BreadcrumbHiddenOne => "breadcrumb.hidden-one", "1 hidden level";
BreadcrumbHiddenMany => "breadcrumb.hidden-many", "{0} hidden levels";
AnchorMoreSections => "anchor.more-sections", "More sections";
PaginationFirst => "pagination.first", "First page";
PaginationPrevious => "pagination.previous", "Previous page";
PaginationNext => "pagination.next", "Next page";
PaginationLast => "pagination.last", "Last page";
PaginationMorePages => "pagination.more-pages", "{0} more pages";
PaginationPageOfTotal => "pagination.page-of-total", "Page {0} of {1}";
PaginationPage => "pagination.page", "Page {0}";
WizardBack => "wizard.back", "Back";
WizardNext => "wizard.next", "Next";
WizardFinish => "wizard.finish", "Finish";
WizardReturnsTo => "wizard.returns-to", "Returns to";
ImageViewerNotSupplied => "image-viewer.not-supplied", "Not supplied — {0}";
ImageViewerPrevious => "image-viewer.previous", "Previous image";
ImageViewerNext => "image-viewer.next", "Next image";
ImageViewerContain => "image-viewer.contain", "Contain";
ImageViewerCover => "image-viewer.cover", "Cover";
ImageViewerSizeUnknown => "image-viewer.size-unknown", "Size unknown";
ImageViewerEmpty => "image-viewer.empty", "No images";
MarkdownImageAlt => "markdown.image-alt", "Image";
MarkdownImageNotFetched => "markdown.image-not-fetched", "Not fetched — {0}";
MarkdownPlainText => "markdown.plain-text", "plain text";
MarkdownTask => "markdown.task", "Task";
MarkdownUnrenderedHtml => "markdown.unrendered-html", "unrendered html";
MarkdownShowMoreOne => "markdown.show-more-one", "Show 1 more line";
MarkdownShowMoreMany => "markdown.show-more-many", "Show {0} more lines";
MessageSending => "message.sending", "Sending";
MessageSent => "message.sent", "Sent";
MessageDelivered => "message.delivered", "Delivered";
MessageRead => "message.read", "Read";
MessageStreaming => "message.streaming", "Streaming";
MessageMoreOne => "message.more-one", "1 more message";
MessageMoreMany => "message.more-many", "{0} more messages";
MessageNewOne => "message.new-one", "1 new message";
MessageNewMany => "message.new-many", "{0} new messages";
MessageShowMoreOne => "message.show-more-one", "1 more line";
MessageShowMoreMany => "message.show-more-many", "{0} more lines";
TimeUnknown => "time.unknown", "Time unknown";
TransportBuffered => "transport.buffered", "Buffered";
TransportTimeUnknown => "transport.time-unknown", "Time unknown";
TransportDurationUnknown => "transport.duration-unknown", "Duration unknown";
TransportPosition => "transport.position", "Playback position";
TransportPlay => "transport.play", "Play";
TransportPause => "transport.pause", "Pause";
TransportPlaying => "transport.playing", "Playing";
TransportPaused => "transport.paused", "Paused";
TransportBuffering => "transport.buffering", "Waiting for data";
TransportMute => "transport.mute", "Mute";
TransportUnmute => "transport.unmute", "Unmute";
TransportVolume => "transport.volume", "Volume";
TransportPreviousTrack => "transport.previous-track", "Previous track";
TransportNextTrack => "transport.next-track", "Next track";
MediaFixture => "media.fixture", "Fixture";
MediaNoTransport => "media.no-transport", "No player";
MediaNoTransportDetail => "media.no-transport-detail", "No player is connected to this surface, so there is nothing to start.";
MediaNoBackend => "media.no-backend", "No playback backend";
MediaFailed => "media.failed", "This could not be played";
MediaEmpty => "media.empty", "Nothing loaded";
MediaWaveform => "media.waveform", "Waveform";
VideoNoFrames => "video.no-frames", "No picture";
VideoNoFramesDetail => "video.no-frames-detail", "The transport holds this video and no frames have been supplied for it.";
ModelEmpty => "model.empty", "No model";
ModelEmptyDetail => "model.empty-detail", "Nothing has been handed to this viewer.";
ModelRefused => "model.refused", "This model was refused";
ModelTooLarge => "model.too-large", "Too many {0}: it asks for {1}, and the limit is {2}.";
ModelRejected => "model.rejected", "It is outside the subset this reader accepts ({0}).";
ModelFlat => "model.flat", "Flat";
ModelWireframe => "model.wireframe", "Wireframe";
ModelReset => "model.reset", "Reset the view";
ModelCount => "model.count", "{0} {1}";
ModelMeshes => "model.meshes", "Meshes";
ModelVertices => "model.vertices", "Vertices";
ModelTriangles => "model.triangles", "Triangles";
SelectPlaceholder => "select.placeholder", "Select";
ComboboxNoMatch => "combobox.no-match", "Nothing here answers “{0}”";
ComboboxCreateHint => "combobox.create-hint", "Press enter to add it as a new value.";
ComboboxClosedHint => "combobox.closed-hint", "This field only accepts one of the options offered.";
CascaderPlaceholder => "cascader.placeholder", "Select";
CascaderUnstarted => "cascader.unstarted", "Nothing has been asked for yet";
CascaderEmpty => "cascader.empty", "No options";
CascaderUnavailable => "cascader.unavailable", "Options unavailable";
CascaderError => "cascader.error", "Could not load options";
DropzoneRefusal => "dropzone.refusal", "This zone does not take that.";
FilterBarLabel => "filter-bar.label", "Filters";
FilterBarAdd => "filter-bar.add", "Add filter";
FilterBarClear => "filter-bar.clear", "Clear all";
FilterBarCounting => "filter-bar.counting", "Counting…";
FilterBarResultsNoun => "filter-bar.results-noun", "results";
InlineEditPlaceholder => "inline-edit.placeholder", "Empty";
KeybindingUnbound => "keybinding.unbound", "Not bound";
KeybindingPrompt => "keybinding.prompt", "Press a shortcut";
KeymapAdd => "keymap.add", "Add binding";
KeymapRemove => "keymap.remove", "Remove";
KeymapReset => "keymap.reset", "Reset to defaults";
KeymapEffective => "keymap.effective", "Current bindings";
KeymapDefaults => "keymap.defaults", "Defaults";
KeymapResultCount => "keymap.result-count", "{0} commands";
NumberDecrease => "number.decrease", "Decrease";
NumberIncrease => "number.increase", "Increase";
NumberNotANumber => "number.not-a-number", "This is not a number.";
NumberBelowMinimum => "number.below-minimum", "The smallest accepted value is {0}.";
NumberAboveMaximum => "number.above-maximum", "The largest accepted value is {0}.";
SettingsManagedBy => "settings.managed-by", "Managed by {0}";
SettingsInapplicable => "settings.inapplicable", "Not available here";
TagInputPlaceholder => "tag-input.placeholder", "Add";
TagInputDuplicate => "tag-input.duplicate", "“{0}” is already here";
TagInputFull => "tag-input.full", "This field holds at most {0}; “{1}” was not added";
TagInputUsed => "tag-input.used", "{0} of {1} used";
TagRemove => "tag.remove", "Remove {0}";
DescriptionUnknown => "description.unknown", "Unknown";
DescriptionNotApplicable => "description.not-applicable", "Not applicable";
DescriptionCopy => "description.copy", "Copy {0}";
DescriptionCharacters => "description.characters", "{0} characters";
CountOfTotal => "common.count-of-total", "{0} of {1}";
PalettePlaceholder => "palette.placeholder", "Type a command";
PaletteNoMatch => "palette.no-match", "No command matches “{0}”";
PaletteEmptyDetail => "palette.empty-detail", "Every command this application was given is listed here.";
KbdSuper => "kbd.super", "Win";
KbdControl => "kbd.control", "Ctrl";
KbdAlt => "kbd.alt", "Alt";
KbdShift => "kbd.shift", "Shift";
GridSelectedNoun => "grid.selected-noun", "selected";
GridSelectAllLoaded => "grid.select-all-loaded", "Select all loaded rows";
GridSelectAllTotal => "grid.select-all-total", "Select all {0}";
GridClearSelection => "grid.clear-selection", "Clear selection";
GridResizeColumn => "grid.resize-column", "Resize {0}";
GridLoadingRows => "grid.loading-rows", "Loading rows";
GridLoadFailed => "grid.load-failed", "Could not load rows";
GridEmpty => "grid.empty", "No rows";
DiagnosticsUnstarted => "diagnostics.unstarted", "Diagnostics have not been requested";
DiagnosticsEmpty => "diagnostics.empty", "No diagnostics";
DiagnosticsNoMatch => "diagnostics.no-match", "No diagnostics match these filters";
DiagnosticsUnavailable => "diagnostics.unavailable", "Diagnostics unavailable";
DiagnosticsError => "diagnostics.error", "Could not load diagnostics";
DiagnosticsFilterField => "diagnostics.filter-field", "Severity";
DiagnosticsFilterOperator => "diagnostics.filter-operator", "is";
DiagnosticsSeverityError => "diagnostics.severity-error", "Error";
DiagnosticsSeverityWarning => "diagnostics.severity-warning", "Warning";
DiagnosticsSeverityInformation => "diagnostics.severity-information", "Information";
DiagnosticsSeverityHint => "diagnostics.severity-hint", "Hint";
DragFileOne => "drag.file-one", "1 file";
DragFileMany => "drag.file-many", "{0} files";
CopyDone => "copy.done", "Copied";
CopyFailed => "copy.failed", "Not copied";
CopyFailedDetail => "copy.failed-detail", "The clipboard did not take it.";
ApprovalDecline => "approval.decline", "Decline";
ApprovalApproveOnce => "approval.approve-once", "Approve once";
ApprovalAlwaysSession => "approval.always-session", "Always for this session";
ApprovalAlwaysTool => "approval.always-tool", "Always for {0}";
ApprovalAlwaysPath => "approval.always-path", "Always in {0}";
ApprovalAlwaysHost => "approval.always-host", "Always on {0}";
ApprovalPending => "approval.pending", "Waiting for your answer";
ApprovalDeclined => "approval.declined", "Declined";
ApprovalApproved => "approval.approved", "Approved: {0}";
ApprovalOnceScope => "approval.once-scope", "this time only";
ApprovalExpired => "approval.expired", "This request expired before it was answered";
ApprovalSuperseded => "approval.superseded", "Replaced by {0}";
PermissionAllowed => "permission.allowed", "Allowed";
PermissionDenied => "permission.denied", "Denied";
PermissionAsk => "permission.ask", "Ask every time";
PermissionNotApplicable => "permission.not-applicable", "Does not apply";
PermissionSubjectHeading => "permission.subject-heading", "Subject";
PermissionInherited => "permission.inherited", "Inherited from {0}";
PermissionSetHere => "permission.set-here", "Set here";
PermissionCellName => "permission.cell-name", "{0}: {1}";
CostMeasured => "cost.measured", "{0}";
CostEstimated => "cost.estimated", "{0} (estimated)";
CostEstimateMark => "cost.estimate-mark", "Estimate";
CostUnavailable => "cost.unavailable", "Unavailable";
CostLastVerified => "cost.last-verified", "Last verified {0}";
ContextUnknownLimit => "context.unknown-limit", "Limit unknown";
JsonWithheld => "json.withheld", "withheld";
JsonRootValue => "json.root-value", "Value";
JsonShapeEntries => "json.shape-entries", "{0} entries";
JsonShapeItems => "json.shape-items", "{0} items";
JsonShapeValue => "json.shape-value", "a value";
SchemaUnrenderable => "schema.unrenderable", "This field cannot be shown here, so it has to be filled in some other way.";
SchemaUnrenderableRequired => "schema.unrenderable-required", "This field is required and cannot be shown here, so this form cannot complete the call.";
SchemaNoChoices => "schema.no-choices", "No choices were offered, so there is nothing to pick.";
SchemaRequiredMissing => "schema.required-missing", "This field is required.";
SchemaUnrenderableOne => "schema.unrenderable-one", "1 field cannot be shown here.";
SchemaUnrenderableMany => "schema.unrenderable-many", "{0} fields cannot be shown here.";
ServerConnected => "server.connected", "Connected";
ServerConnecting => "server.connecting", "Connecting";
ServerDisconnected => "server.disconnected", "Disconnected";
ServerFailed => "server.failed", "Failed";
ServerDisabled => "server.disabled", "Turned off";
ServerTools => "server.tools", "Tools";
ServerSkills => "server.skills", "Skills";
ServerResources => "server.resources", "Resources";
ServerOfferingsUnasked => "server.offerings-unasked", "Nothing has been asked for yet";
ServerOfferingsUnaskedDetail => "server.offerings-unasked-detail", "This connection has not been asked what it offers.";
ServerOfferingsAsking => "server.offerings-asking", "Asking what this connection offers";
ServerOfferingsNone => "server.offerings-none", "This connection offers nothing";
ServerOfferingsNoneDetail => "server.offerings-none-detail", "It answered, and the answer was empty.";
ServerOfferingsUnavailable => "server.offerings-unavailable", "What this connection offers is unknown";
ServerEmpty => "server.empty", "No connections";
ServerEmptyDetail => "server.empty-detail", "Nothing has been connected yet.";
AgentArguments => "agent.arguments", "Arguments";
AgentResult => "agent.result", "Result";
AgentPendingApproval => "agent.pending-approval", "Waiting for approval";
AgentRunning => "agent.running", "Running";
AgentSucceeded => "agent.succeeded", "Succeeded";
AgentFailed => "agent.failed", "Failed";
AgentDeclined => "agent.declined", "Declined";
AgentPending => "agent.pending", "Pending";
AgentDone => "agent.done", "Done";
AgentSkipped => "agent.skipped", "Skipped";
AgentNoOutput => "agent.no-output", "This tool returned nothing";
AgentElapsedUnknown => "agent.elapsed-unknown", "Elapsed time unknown";
AgentTruncated => "agent.truncated", "{0} of {1} lines shown";
AgentLinesOne => "agent.lines-one", "1 line";
AgentLinesMany => "agent.lines-many", "{0} lines";
AgentStepsDoneOne => "agent.steps-done-one", "1 step done";
AgentStepsDoneMany => "agent.steps-done-many", "{0} steps done";
AgentReasoning => "agent.reasoning", "Reasoning";
AgentReasoningWithheld => "agent.reasoning-withheld", "Withheld";
AgentReasoningThinking => "agent.reasoning-thinking", "Thinking";
AgentReasoningAbsent => "agent.reasoning-absent", "No reasoning was returned";
TabDirty => "tab.dirty", "Unsaved changes";
TabSaving => "tab.saving", "Saving";
TabSaveFailed => "tab.save-failed", "Could not save";
TabClose => "tab.close", "Close {0}";
TabMoreTabs => "tab.more-tabs", "More tabs";
SearchPlaceholder => "search.placeholder", "Find";
SearchNoHits => "search.no-hits", "No results";
SearchCounting => "search.counting", "Counting…";
SearchNotSearched => "search.not-searched", "Nothing searched yet";
SearchTooMany => "search.too-many", "More than {0}";
SearchHitOne => "search.hit-one", "1 result";
SearchHitMany => "search.hit-many", "{0} results";
SearchNext => "search.next", "Next result";
SearchPrevious => "search.previous", "Previous result";
SearchCaseSensitive => "search.case-sensitive", "Match case";
SearchWholeWord => "search.whole-word", "Whole word";
ReplacePlaceholder => "replace.placeholder", "Replace with";
ReplaceOne => "replace.one", "Replace";
ReplaceAllCounted => "replace.all-counted", "Replace all {0}";
ReplaceAllUncounted => "replace.all-uncounted", "Replace all";
ReplaceAllUncountable => "replace.all-uncountable", "Nobody has counted the results yet, so this cannot say how many it would change.";
NotificationsTitle => "notifications.title", "Notifications";
NotificationsEmpty => "notifications.empty", "Nothing to report";
NotificationsEmptyDetail => "notifications.empty-detail", "Notifications that have come and gone are kept here.";
NotificationsClearAll => "notifications.clear-all", "Clear all";
NotificationsMarkAllRead => "notifications.mark-all-read", "Mark all as read";
NotificationsUnread => "notifications.unread", "Unread";
NotificationsUnreadCount => "notifications.unread-count", "{0} unread";
NotificationsAtLeast => "notifications.at-least", "{0}+";
FailureTitle => "failure.title", "This panel could not be shown";
FailureAttempts => "failure.attempts", "Tried {0} times";
FailureRetrying => "failure.retrying", "Trying again";
CodeLineNumbers => "code.line-numbers", "Line numbers";
CodeLineAdded => "code.line-added", "Added";
CodeLineRemoved => "code.line-removed", "Removed";
CodeLineChanged => "code.line-changed", "Changed";
CodeLineHighlighted => "code.line-highlighted", "Highlighted";
CodeLineError => "code.line-error", "Error";
CodeEmpty => "code.empty", "Nothing to show";
LogFollow => "log.follow", "Follow output";
LogPause => "log.pause", "Pause output";
LogFollowing => "log.following", "Following newest";
LogPaused => "log.paused", "Follow paused";
LogEmpty => "log.empty", "No log entries";
LogUnavailable => "log.unavailable", "Log unavailable";
LogError => "log.error", "Could not load log";
DiffFile => "diff.file", "File";
DiffHunk => "diff.hunk", "Hunk";
DiffContextLine => "diff.context-line", "Context line";
DiffChangedLine => "diff.changed-line", "Changed line";
DiffEmpty => "diff.empty", "No differences";
SparklineEmpty => "sparkline.empty", "No readings";
SparklineUnavailable => "sparkline.unavailable", "Reading unavailable";
SparklineError => "sparkline.error", "Could not load reading";
SparklineCurrent => "sparkline.current", "Current: {0}";
SparklineMinimum => "sparkline.minimum", "Minimum: {0}";
SparklineMaximum => "sparkline.maximum", "Maximum: {0}";
SparklineRange => "sparkline.range", "Minimum {0}; maximum {1}";
UploadQueued => "upload.queued", "Queued";
UploadUploading => "upload.uploading", "Uploading";
UploadDone => "upload.done", "Uploaded";
UploadFailed => "upload.failed", "Failed";
UploadCancelled => "upload.cancelled", "Cancelled";
UploadRefused => "upload.refused", "Not accepted";
UploadCancel => "upload.cancel", "Cancel {0}";
UploadRemove => "upload.remove", "Remove {0}";
UploadOverall => "upload.overall", "Uploading";
UploadEmpty => "upload.empty", "No files yet";
BrowserPanel => "browser.panel", "Browser";
BrowserBack => "browser.back", "Back";
BrowserForward => "browser.forward", "Forward";
BrowserReload => "browser.reload", "Reload";
BrowserNoAddress => "browser.no-address", "No address";
BrowserEmpty => "browser.empty", "No page content";
BrowserEmptyDetail => "browser.empty-detail", "The page loaded without content.";
BrowserUnavailable => "browser.unavailable", "Web content unavailable";
BrowserNoEngineDetail => "browser.no-engine-detail", "This build cannot display web content.";
BrowserError => "browser.error", "Could not load";
BrowserNoViewport => "browser.no-viewport", "No page surface";
BrowserNoViewportDetail => "browser.no-viewport-detail",
"The host reported a ready page but supplied no viewport.";
OfferingCatalogEmpty => "offering-catalog.empty", "No offerings";
OfferingCatalogNoMatch => "offering-catalog.no-match", "No matching offerings";
OfferingSourceLoading => "offering-source.loading", "{0}: Loading offerings";
OfferingSourceEmpty => "offering-source.empty", "{0}: No offerings";
OfferingSourceUnavailable => "offering-source.unavailable", "{0}: Offerings unavailable";
OfferingSourceError => "offering-source.error", "{0}: Could not load offerings";
OfferingSourceStale => "offering-source.stale", "{0}: Showing last verified offerings";
}
#[derive(Debug, Clone, Default)]
pub struct Strings {
overrides: BTreeMap<StringKey, SharedString>,
}
impl Global for Strings {}
impl Strings {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: StringKey, text: impl Into<SharedString>) -> &mut Self {
self.overrides.insert(key, text.into());
self
}
pub fn clear(&mut self, key: StringKey) -> &mut Self {
self.overrides.remove(&key);
self
}
pub fn clear_all(&mut self) -> &mut Self {
self.overrides.clear();
self
}
pub fn extend(
&mut self,
entries: impl IntoIterator<Item = (StringKey, SharedString)>,
) -> &mut Self {
self.overrides.extend(entries);
self
}
pub fn is_overridden(&self, key: StringKey) -> bool {
self.overrides.contains_key(&key)
}
pub fn text(&self, key: StringKey) -> SharedString {
match self.overrides.get(&key) {
Some(text) => text.clone(),
None => SharedString::new_static(key.english()),
}
}
pub fn format(&self, key: StringKey, args: &[&str]) -> SharedString {
SharedString::from(interpolate(self.text(key).as_ref(), args))
}
}
fn interpolate(template: &str, args: &[&str]) -> String {
let mut out = String::with_capacity(template.len());
let mut rest = template;
while let Some(open) = rest.find('{') {
let (before, tail) = rest.split_at(open);
out.push_str(before);
let Some(close) = tail.find('}') else {
out.push_str(tail);
return out;
};
let slot = &tail[1..close];
match slot.parse::<usize>().ok().and_then(|index| args.get(index)) {
Some(value) => out.push_str(value),
None => out.push_str(&tail[..=close]),
}
rest = &tail[close + 1..];
}
out.push_str(rest);
out
}
pub trait ActiveStrings {
fn strings(&self) -> &Strings;
}
impl ActiveStrings for App {
fn strings(&self) -> &Strings {
static ENGLISH: OnceLock<Strings> = OnceLock::new();
self.try_global::<Strings>()
.unwrap_or_else(|| ENGLISH.get_or_init(Strings::new))
}
}
pub fn install(cx: &mut App) {
if !cx.has_global::<Strings>() {
cx.set_global(Strings::new());
}
}
pub fn set_strings(entries: impl IntoIterator<Item = (StringKey, SharedString)>, cx: &mut App) {
install(cx);
cx.update_global::<Strings, ()>(|strings, _| {
strings.extend(entries);
});
cx.refresh_windows();
}
pub fn reset_strings(cx: &mut App) {
install(cx);
cx.update_global::<Strings, ()>(|strings, _| {
strings.clear_all();
});
cx.refresh_windows();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_key_has_a_unique_name_and_english() {
let mut names: Vec<&str> = StringKey::ALL.iter().map(|key| key.name()).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(names.len(), before, "two keys share a name");
for key in StringKey::ALL {
assert!(!key.english().is_empty(), "{} has no English", key.name());
assert_eq!(StringKey::from_name(key.name()), Some(*key));
}
}
#[test]
fn an_empty_catalogue_answers_in_english() {
let strings = Strings::new();
assert_eq!(strings.text(StringKey::Copy), "Copy");
assert_eq!(strings.text(StringKey::TryAgain), "Try again");
}
#[test]
fn an_override_replaces_only_what_it_names() {
let mut strings = Strings::new();
strings.set(StringKey::Copy, "Kopieren");
assert_eq!(strings.text(StringKey::Copy), "Kopieren");
assert_eq!(strings.text(StringKey::TryAgain), "Try again");
strings.clear(StringKey::Copy);
assert_eq!(strings.text(StringKey::Copy), "Copy");
}
#[test]
fn a_template_takes_its_arguments_in_any_order() {
let mut strings = Strings::new();
assert_eq!(
strings.format(StringKey::RangeComplete, &["Monday", "Friday"]),
"Monday to Friday."
);
strings.set(StringKey::RangeComplete, "{1} back to {0}.");
assert_eq!(
strings.format(StringKey::RangeComplete, &["Monday", "Friday"]),
"Friday back to Monday."
);
}
#[test]
fn a_placeholder_with_no_argument_stays_visible() {
let strings = Strings::new();
assert_eq!(
strings.format(StringKey::RangeComplete, &["Monday"]),
"Monday to {1}."
);
}
#[test]
fn every_placeholder_in_the_english_is_numbered_from_zero() {
for key in StringKey::ALL {
let english = key.english();
let mut expected = 0usize;
let mut rest = english;
while let Some(open) = rest.find('{') {
let tail = &rest[open..];
let close = tail.find('}').unwrap_or_else(|| {
panic!("{} has an unclosed placeholder", key.name());
});
let slot = &tail[1..close];
assert_eq!(
slot.parse::<usize>().ok(),
Some(expected),
"{} numbers its placeholders out of order",
key.name()
);
expected += 1;
rest = &tail[close + 1..];
}
}
}
}