1use fluent_bundle::concurrent::FluentBundle;
8use fluent_bundle::{FluentArgs, FluentResource};
9use std::env;
10use std::str::FromStr;
11use std::sync::OnceLock;
12use unic_langid::LanguageIdentifier;
13
14const EN_US: &str = "en-US";
15
16const EN_MESSAGES: &str = include_str!("../locales/en-US.ftl");
17const JA_MESSAGES: &str = include_str!("../locales/ja-JP.ftl");
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Locale {
22 English,
24 Japanese,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Message {
33 ErrorPrefix,
34 InitializedBoardAt,
35 AlreadyInitialized,
36 Created,
37 NoBacklogItems,
38 NoActionableItems,
39 Moved,
40 AcceptanceCriteriaIncomplete,
41 Reordered,
42 Updated,
43 NoChangesTo,
44 Archived,
45 Restored,
46 Deleted,
47 CreatedSprint,
48 UpdatedSprint,
49 DeletedSprint,
50 StartedSprint,
51 ClosedSprint,
52 AssignedToSprint,
53 UnassignedFromSprint,
54 NoSprints,
55 DependencyAdded,
56 DependencyCycleWarning,
57 DependencyCycleWarningGeneric,
58 DependencyRemoved,
59 LinkAlreadyLinked,
60 LinkAdded,
61 LinkNoMatchingCommit,
62 LinkRemoved,
63 LinkNoNewCommits,
64 LinkCommitLinked,
65 LinkSummary,
66 DodUnset,
67 DodUpdated,
68 DodCleared,
69 DodNoCommonToClear,
70 MigrationCompleted,
71 MigrationBackendUpdated,
72 MigrationAlreadyUsing,
73 MigrationSqliteUnavailable,
74 WipLimitExceeded,
75 SprintLoadWarning,
76 OrphanedItemsWarning,
77 RebalanceAlreadyBalanced,
78 RebalanceDryRun,
79 RebalanceCompleted,
80 DoctorHealthy,
81 DoctorIssues,
82 DoctorFixed,
83 DoctorIssue,
84 DoctorKindDanglingDependency,
85 DoctorKindDanglingParent,
86 DoctorKindDanglingSprint,
87 DoctorKindParentCycle,
88 DoctorKindDependencyCycle,
89 DoctorKindDuplicateId,
90 DoctorKindIssuedId,
91 DoctorKindInvalidStatus,
92 DoctorKindRankAnomaly,
93 DoctorKindCollision,
94 DoctorKindMalformedRecord,
95 DoctorKindFilename,
96 InvalidCapacityOptions,
97 FailedToAction,
98 ShellParseError,
99 SprintAddRequiresItemOrStatus,
100 ErrorInvalidItemId,
101 ErrorInvalidRank,
102 ErrorInvalidAutomationPlan,
103 ErrorAutomationPlanSource,
104 ErrorUnknownStatus,
105 ErrorEmptyTitle,
106 ErrorInvalidSearchPattern,
107 ErrorInvalidFilterOption,
108 ErrorNothingToUpdate,
109 ErrorEmptyDod,
110 ErrorInvalidTemplateName,
111 ErrorTemplateNotFound,
112 ErrorTemplateUnreadable,
113 ErrorNotFound,
114 ErrorReferencedItem,
115 ErrorInvalidSprintId,
116 ErrorInvalidSprintState,
117 ErrorEmptySprintTitle,
118 ErrorEmptySprintGoal,
119 ErrorInvalidSprintTransition,
120 ErrorSprintNotFound,
121 ErrorSprintExists,
122 ErrorSprintClosed,
123 ErrorInvalidSprintPeriod,
124 ErrorInvalidDailyWorkHours,
125 ErrorInvalidDeductionFactor,
126 ErrorInvalidSprintHolidays,
127 ErrorSprintCapacityPeriodUnset,
128 ErrorSprintCapacityUnset,
129 ErrorSprintPeriodUnset,
130 ErrorSprintEmpty,
131 ErrorNotInSprint,
132 ErrorParentCycle,
133 ErrorNotInitialized,
134 ErrorIo,
135 ErrorParse,
136 ErrorMissingFrontmatter,
137 ErrorUnsupportedSqliteSchema,
138 ErrorSelfReference,
139 ErrorNotSibling,
140 ErrorTask,
141 ErrorGit,
142 ErrorEditorNotSet,
143 ErrorEditorLaunch,
144 ErrorEditorInvalid,
145 ErrorLocked,
146 AlreadyInInteractiveShell,
147 KanbanDetailsTitle,
148 KanbanPopupHints,
149 KanbanHelpTitle,
150 KanbanHelpEntries,
151 KanbanNoChanges,
152 KanbanEditorFailed,
153 KanbanEditFailed,
154 KanbanWipExceeded,
155 KanbanNoEditor,
156 KanbanKeyHints,
157 KanbanHelpHint,
158 KanbanHelpClearFilter,
159 KanbanAddTitlePrompt,
160 KanbanAddBodyPrompt,
161 KanbanAddParentPrompt,
162 KanbanAddDependenciesPrompt,
163 KanbanDependencyAddPrompt,
164 KanbanDependencyRemovePrompt,
165 KanbanEmptyTitle,
166 KanbanEmptyDependency,
167 KanbanDependencyAdded,
168 KanbanDependencyRemoved,
169 KanbanDependencyCycleWarning,
170 KanbanParentPrompt,
171 KanbanParentSet,
172 KanbanParentCleared,
173 KanbanActiveFilter,
174 KanbanActiveRegexFilter,
175 KanbanDependencyLegend,
176 KanbanColumnRange,
177 KanbanEmptyColumns,
178 KanbanQuitBody,
179 KanbanQuitPrompt,
180 KanbanNoBody,
181 KanbanNoSelection,
182 KanbanTerminalInitFailed,
183 EditGuidance,
184 AutomationCommandValid,
185 AutomationCommandInvalid,
186 AutomationCommandFailed,
187 AutomationCommandSkipped,
188 AutomationDryRunCompleted,
189 AutomationCompleted,
190 AutomationPartialFailure,
191 AutomationInvalidCommandArguments,
192 AutomationNotExecutedAfterFailure,
193 AutomationNotValidatedAfterFailure,
194 AutomationDryRunGitInitFailed,
195 AutomationDryRunWorkspaceUnavailable,
196 AutomationCommandExited,
197}
198
199impl Message {
200 const fn id(self) -> &'static str {
201 match self {
202 Self::ErrorPrefix => "error-prefix",
203 Self::InitializedBoardAt => "initialized-board-at",
204 Self::AlreadyInitialized => "already-initialized",
205 Self::Created => "created",
206 Self::NoBacklogItems => "no-backlog-items",
207 Self::NoActionableItems => "no-actionable-items",
208 Self::Moved => "moved",
209 Self::AcceptanceCriteriaIncomplete => "acceptance-criteria-incomplete",
210 Self::Reordered => "reordered",
211 Self::Updated => "updated",
212 Self::NoChangesTo => "no-changes-to",
213 Self::Archived => "archived",
214 Self::Restored => "restored",
215 Self::Deleted => "deleted",
216 Self::CreatedSprint => "created-sprint",
217 Self::UpdatedSprint => "updated-sprint",
218 Self::DeletedSprint => "deleted-sprint",
219 Self::StartedSprint => "started-sprint",
220 Self::ClosedSprint => "closed-sprint",
221 Self::AssignedToSprint => "assigned-to-sprint",
222 Self::UnassignedFromSprint => "unassigned-from-sprint",
223 Self::NoSprints => "no-sprints",
224 Self::DependencyAdded => "dependency-added",
225 Self::DependencyCycleWarning => "dependency-cycle-warning",
226 Self::DependencyCycleWarningGeneric => "dependency-cycle-warning-generic",
227 Self::DependencyRemoved => "dependency-removed",
228 Self::LinkAlreadyLinked => "link-already-linked",
229 Self::LinkAdded => "link-added",
230 Self::LinkNoMatchingCommit => "link-no-matching-commit",
231 Self::LinkRemoved => "link-removed",
232 Self::LinkNoNewCommits => "link-no-new-commits",
233 Self::LinkCommitLinked => "link-commit-linked",
234 Self::LinkSummary => "link-summary",
235 Self::DodUnset => "dod-unset",
236 Self::DodUpdated => "dod-updated",
237 Self::DodCleared => "dod-cleared",
238 Self::DodNoCommonToClear => "dod-no-common-to-clear",
239 Self::MigrationCompleted => "migration-completed",
240 Self::MigrationBackendUpdated => "migration-backend-updated",
241 Self::MigrationAlreadyUsing => "migration-already-using",
242 Self::MigrationSqliteUnavailable => "migration-sqlite-unavailable",
243 Self::WipLimitExceeded => "wip-limit-exceeded",
244 Self::SprintLoadWarning => "sprint-load-warning",
245 Self::OrphanedItemsWarning => "orphaned-items-warning",
246 Self::RebalanceAlreadyBalanced => "rebalance-already-balanced",
247 Self::RebalanceDryRun => "rebalance-dry-run",
248 Self::RebalanceCompleted => "rebalance-completed",
249 Self::DoctorHealthy => "doctor-healthy",
250 Self::DoctorIssues => "doctor-issues",
251 Self::DoctorFixed => "doctor-fixed",
252 Self::DoctorIssue => "doctor-issue",
253 Self::DoctorKindDanglingDependency => "doctor-kind-dangling-dependency",
254 Self::DoctorKindDanglingParent => "doctor-kind-dangling-parent",
255 Self::DoctorKindDanglingSprint => "doctor-kind-dangling-sprint",
256 Self::DoctorKindParentCycle => "doctor-kind-parent-cycle",
257 Self::DoctorKindDependencyCycle => "doctor-kind-dependency-cycle",
258 Self::DoctorKindDuplicateId => "doctor-kind-duplicate-id",
259 Self::DoctorKindIssuedId => "doctor-kind-issued-id",
260 Self::DoctorKindInvalidStatus => "doctor-kind-invalid-status",
261 Self::DoctorKindRankAnomaly => "doctor-kind-rank-anomaly",
262 Self::DoctorKindCollision => "doctor-kind-collision",
263 Self::DoctorKindMalformedRecord => "doctor-kind-malformed-record",
264 Self::DoctorKindFilename => "doctor-kind-filename",
265 Self::InvalidCapacityOptions => "invalid-capacity-options",
266 Self::FailedToAction => "failed-to-action",
267 Self::ShellParseError => "shell-parse-error",
268 Self::SprintAddRequiresItemOrStatus => "sprint-add-requires-item-or-status",
269 Self::ErrorInvalidItemId => "error-invalid-item-id",
270 Self::ErrorInvalidRank => "error-invalid-rank",
271 Self::ErrorInvalidAutomationPlan => "error-invalid-automation-plan",
272 Self::ErrorAutomationPlanSource => "error-automation-plan-source",
273 Self::ErrorUnknownStatus => "error-unknown-status",
274 Self::ErrorEmptyTitle => "error-empty-title",
275 Self::ErrorInvalidSearchPattern => "error-invalid-search-pattern",
276 Self::ErrorInvalidFilterOption => "error-invalid-filter-option",
277 Self::ErrorNothingToUpdate => "error-nothing-to-update",
278 Self::ErrorEmptyDod => "error-empty-dod",
279 Self::ErrorInvalidTemplateName => "error-invalid-template-name",
280 Self::ErrorTemplateNotFound => "error-template-not-found",
281 Self::ErrorTemplateUnreadable => "error-template-unreadable",
282 Self::ErrorNotFound => "error-not-found",
283 Self::ErrorReferencedItem => "error-referenced-item",
284 Self::ErrorInvalidSprintId => "error-invalid-sprint-id",
285 Self::ErrorInvalidSprintState => "error-invalid-sprint-state",
286 Self::ErrorEmptySprintTitle => "error-empty-sprint-title",
287 Self::ErrorEmptySprintGoal => "error-empty-sprint-goal",
288 Self::ErrorInvalidSprintTransition => "error-invalid-sprint-transition",
289 Self::ErrorSprintNotFound => "error-sprint-not-found",
290 Self::ErrorSprintExists => "error-sprint-exists",
291 Self::ErrorSprintClosed => "error-sprint-closed",
292 Self::ErrorInvalidSprintPeriod => "error-invalid-sprint-period",
293 Self::ErrorInvalidDailyWorkHours => "error-invalid-daily-work-hours",
294 Self::ErrorInvalidDeductionFactor => "error-invalid-deduction-factor",
295 Self::ErrorInvalidSprintHolidays => "error-invalid-sprint-holidays",
296 Self::ErrorSprintCapacityPeriodUnset => "error-sprint-capacity-period-unset",
297 Self::ErrorSprintCapacityUnset => "error-sprint-capacity-unset",
298 Self::ErrorSprintPeriodUnset => "error-sprint-period-unset",
299 Self::ErrorSprintEmpty => "error-sprint-empty",
300 Self::ErrorNotInSprint => "error-not-in-sprint",
301 Self::ErrorParentCycle => "error-parent-cycle",
302 Self::ErrorNotInitialized => "error-not-initialized",
303 Self::ErrorIo => "error-io",
304 Self::ErrorParse => "error-parse",
305 Self::ErrorMissingFrontmatter => "error-missing-frontmatter",
306 Self::ErrorUnsupportedSqliteSchema => "error-unsupported-sqlite-schema",
307 Self::ErrorSelfReference => "error-self-reference",
308 Self::ErrorNotSibling => "error-not-sibling",
309 Self::ErrorTask => "error-task",
310 Self::ErrorGit => "error-git",
311 Self::ErrorEditorNotSet => "error-editor-not-set",
312 Self::ErrorEditorLaunch => "error-editor-launch",
313 Self::ErrorEditorInvalid => "error-editor-invalid",
314 Self::ErrorLocked => "error-locked",
315 Self::AlreadyInInteractiveShell => "already-in-interactive-shell",
316 Self::KanbanDetailsTitle => "kanban-details-title",
317 Self::KanbanPopupHints => "kanban-popup-hints",
318 Self::KanbanHelpTitle => "kanban-help-title",
319 Self::KanbanHelpEntries => "kanban-help-entries",
320 Self::KanbanNoChanges => "kanban-no-changes",
321 Self::KanbanEditorFailed => "kanban-editor-failed",
322 Self::KanbanEditFailed => "kanban-edit-failed",
323 Self::KanbanWipExceeded => "kanban-wip-exceeded",
324 Self::KanbanNoEditor => "kanban-no-editor",
325 Self::KanbanKeyHints => "kanban-key-hints",
326 Self::KanbanHelpHint => "kanban-help-hint",
327 Self::KanbanHelpClearFilter => "kanban-help-clear-filter",
328 Self::KanbanAddTitlePrompt => "kanban-add-title-prompt",
329 Self::KanbanAddBodyPrompt => "kanban-add-body-prompt",
330 Self::KanbanAddParentPrompt => "kanban-add-parent-prompt",
331 Self::KanbanAddDependenciesPrompt => "kanban-add-dependencies-prompt",
332 Self::KanbanDependencyAddPrompt => "kanban-dependency-add-prompt",
333 Self::KanbanDependencyRemovePrompt => "kanban-dependency-remove-prompt",
334 Self::KanbanEmptyTitle => "kanban-empty-title",
335 Self::KanbanEmptyDependency => "kanban-empty-dependency",
336 Self::KanbanDependencyAdded => "kanban-dependency-added",
337 Self::KanbanDependencyRemoved => "kanban-dependency-removed",
338 Self::KanbanDependencyCycleWarning => "kanban-dependency-cycle-warning",
339 Self::KanbanParentPrompt => "kanban-parent-prompt",
340 Self::KanbanParentSet => "kanban-parent-set",
341 Self::KanbanParentCleared => "kanban-parent-cleared",
342 Self::KanbanActiveFilter => "kanban-active-filter",
343 Self::KanbanActiveRegexFilter => "kanban-active-regex-filter",
344 Self::KanbanDependencyLegend => "kanban-dependency-legend",
345 Self::KanbanColumnRange => "kanban-column-range",
346 Self::KanbanEmptyColumns => "kanban-empty-columns",
347 Self::KanbanQuitBody => "kanban-quit-body",
348 Self::KanbanQuitPrompt => "kanban-quit-prompt",
349 Self::KanbanNoBody => "kanban-no-body",
350 Self::KanbanNoSelection => "kanban-no-selection",
351 Self::KanbanTerminalInitFailed => "kanban-terminal-init-failed",
352 Self::EditGuidance => "edit-guidance",
353 Self::AutomationCommandValid => "automation-command-valid",
354 Self::AutomationCommandInvalid => "automation-command-invalid",
355 Self::AutomationCommandFailed => "automation-command-failed",
356 Self::AutomationCommandSkipped => "automation-command-skipped",
357 Self::AutomationDryRunCompleted => "automation-dry-run-completed",
358 Self::AutomationCompleted => "automation-completed",
359 Self::AutomationPartialFailure => "automation-partial-failure",
360 Self::AutomationInvalidCommandArguments => "automation-invalid-command-arguments",
361 Self::AutomationNotExecutedAfterFailure => "automation-not-executed-after-failure",
362 Self::AutomationNotValidatedAfterFailure => "automation-not-validated-after-failure",
363 Self::AutomationDryRunGitInitFailed => "automation-dry-run-git-init-failed",
364 Self::AutomationDryRunWorkspaceUnavailable => {
365 "automation-dry-run-workspace-unavailable"
366 }
367 Self::AutomationCommandExited => "automation-command-exited",
368 }
369 }
370}
371
372pub struct Localizer {
374 locale: Locale,
375 language: LanguageIdentifier,
376 bundle: FluentBundle<FluentResource>,
377}
378
379impl Localizer {
380 #[must_use]
382 pub fn from_environment() -> Self {
383 localizer_from(
384 env::var("LC_ALL").ok().as_deref(),
385 env::var("LANG").ok().as_deref(),
386 )
387 }
388
389 pub const fn locale(&self) -> Locale {
391 self.locale
392 }
393
394 pub fn language_identifier(&self) -> &LanguageIdentifier {
396 &self.language
397 }
398
399 pub fn text(&self, message: Message) -> String {
401 self.format(message, [])
402 }
403
404 pub fn lookup(&self, id: &str) -> Option<String> {
410 let message = self.bundle.get_message(id)?;
411 let pattern = message.value()?;
412 let mut errors = Vec::new();
413 let value = self.bundle.format_pattern(pattern, None, &mut errors);
414 errors.is_empty().then(|| value.into_owned())
415 }
416
417 pub fn format<'a>(
422 &self,
423 message: Message,
424 variables: impl IntoIterator<Item = (&'a str, &'a str)>,
425 ) -> String {
426 let id = message.id();
427 let Some(message) = self.bundle.get_message(id) else {
428 return id.to_string();
429 };
430 let Some(pattern) = message.value() else {
431 return id.to_string();
432 };
433 let mut args = FluentArgs::new();
434 for (name, value) in variables {
435 args.set(name, value);
436 }
437 let mut errors = Vec::new();
438 let value = self
439 .bundle
440 .format_pattern(pattern, Some(&args), &mut errors);
441 if errors.is_empty() {
442 value.into_owned()
443 } else {
444 id.to_string()
445 }
446 }
447}
448
449#[must_use]
455pub fn locale_name_from<'a>(lc_all: Option<&'a str>, lang: Option<&'a str>) -> Option<&'a str> {
456 lc_all
457 .filter(|value| !value.is_empty())
458 .or_else(|| lang.filter(|value| !value.is_empty()))
459}
460
461#[must_use]
463pub fn localizer_from(lc_all: Option<&str>, lang: Option<&str>) -> Localizer {
464 let (locale, language, messages) = localized_resource(lc_all, lang);
465 let resource = match FluentResource::try_new(messages.to_string()) {
466 Ok(resource) | Err((resource, _)) => resource,
467 };
468 let mut bundle = FluentBundle::new_concurrent(vec![language.clone()]);
469 bundle.set_use_isolating(false);
470 let _ = bundle.add_resource(resource);
471 Localizer {
472 locale,
473 language,
474 bundle,
475 }
476}
477
478fn localized_resource(
484 lc_all: Option<&str>,
485 lang: Option<&str>,
486) -> (Locale, LanguageIdentifier, &'static str) {
487 let candidate = locale_name_from(lc_all, lang)
488 .and_then(|name| name.split('.').next())
489 .map(|name| name.replace('_', "-"))
490 .and_then(|name| LanguageIdentifier::from_str(&name).ok());
491
492 match candidate {
493 Some(language) if language.language.as_str().eq_ignore_ascii_case("en") => {
494 (Locale::English, language, EN_MESSAGES)
495 }
496 Some(language) if language.language.as_str().eq_ignore_ascii_case("ja") => {
497 (Locale::Japanese, language, JA_MESSAGES)
498 }
499 Some(_) | None => (Locale::English, english_fallback(), EN_MESSAGES),
500 }
501}
502
503fn english_fallback() -> LanguageIdentifier {
504 LanguageIdentifier::from_str(EN_US).unwrap_or_default()
507}
508
509#[must_use]
514pub fn current() -> &'static Localizer {
515 static CURRENT_LOCALIZER: OnceLock<Localizer> = OnceLock::new();
516 CURRENT_LOCALIZER.get_or_init(Localizer::from_environment)
517}
518
519#[cfg(test)]
520mod tests {
521 use super::*;
522
523 #[test]
524 fn lc_all_is_selected_before_lang() {
525 assert_eq!(locale_name_from(Some("C"), Some("ja_JP.UTF-8")), Some("C"));
526 }
527
528 #[test]
529 fn empty_lc_all_defers_to_lang() {
530 assert_eq!(
531 locale_name_from(Some(""), Some("en_US.UTF-8")),
532 Some("en_US.UTF-8")
533 );
534 }
535
536 #[test]
537 fn missing_locale_values_use_the_english_fallback() {
538 assert_eq!(locale_name_from(None, None), None);
539 assert_eq!(locale_name_from(Some(""), Some("")), None);
540
541 for (lc_all, lang) in [(None, None), (Some(""), Some("invalid_locale"))] {
542 let localizer = localizer_from(lc_all, lang);
543 assert_eq!(localizer.locale(), Locale::English);
544 assert_eq!(localizer.language_identifier().to_string(), "en-US");
545 }
546 }
547
548 #[test]
549 fn unsupported_locale_uses_english_messages() {
550 let localizer = localizer_from(Some("fr_FR.UTF-8"), None);
551 assert_eq!(localizer.locale(), Locale::English);
552 assert_eq!(localizer.text(Message::NoBacklogItems), "No backlog items.");
553 }
554
555 #[test]
556 fn current_reuses_one_localizer_for_the_process_lifetime() {
557 assert!(std::ptr::eq(current(), current()));
558 }
559
560 #[test]
561 fn edit_guidance_is_localized_and_kept_as_toml_comments() {
562 let en = localizer_from(Some("en_US.UTF-8"), None).text(Message::EditGuidance);
565 let ja = localizer_from(Some("ja_JP.UTF-8"), None).text(Message::EditGuidance);
566
567 for guide in [&en, &ja] {
568 assert_ne!(guide, "edit-guidance", "must resolve from FTL");
570 assert!(guide.starts_with("# pinto:"), "opens with a marker comment");
571 assert!(
572 guide.lines().all(|line| line.starts_with('#')),
573 "all lines are TOML comments: {guide:?}"
574 );
575 }
576 assert!(en.contains("saving applies"), "English guidance is English");
577 assert!(ja.contains("保存すると"), "Japanese guidance is Japanese");
578 assert_ne!(en, ja, "locales produce distinct guidance");
579 }
580
581 #[test]
582 fn every_message_id_has_a_safe_fallback_and_lookup_handles_unknown_ids() {
583 let localizer = localizer_from(Some("en-US"), None);
584 let messages = [
585 Message::ErrorPrefix,
586 Message::InitializedBoardAt,
587 Message::AlreadyInitialized,
588 Message::Created,
589 Message::NoBacklogItems,
590 Message::NoActionableItems,
591 Message::Moved,
592 Message::AcceptanceCriteriaIncomplete,
593 Message::Reordered,
594 Message::Updated,
595 Message::NoChangesTo,
596 Message::Archived,
597 Message::Restored,
598 Message::Deleted,
599 Message::CreatedSprint,
600 Message::UpdatedSprint,
601 Message::DeletedSprint,
602 Message::StartedSprint,
603 Message::ClosedSprint,
604 Message::AssignedToSprint,
605 Message::UnassignedFromSprint,
606 Message::NoSprints,
607 Message::DependencyAdded,
608 Message::DependencyCycleWarning,
609 Message::DependencyCycleWarningGeneric,
610 Message::DependencyRemoved,
611 Message::LinkAlreadyLinked,
612 Message::LinkAdded,
613 Message::LinkNoMatchingCommit,
614 Message::LinkRemoved,
615 Message::LinkNoNewCommits,
616 Message::LinkCommitLinked,
617 Message::LinkSummary,
618 Message::DodUnset,
619 Message::DodUpdated,
620 Message::DodCleared,
621 Message::DodNoCommonToClear,
622 Message::MigrationCompleted,
623 Message::MigrationBackendUpdated,
624 Message::MigrationAlreadyUsing,
625 Message::MigrationSqliteUnavailable,
626 Message::WipLimitExceeded,
627 Message::SprintLoadWarning,
628 Message::OrphanedItemsWarning,
629 Message::RebalanceAlreadyBalanced,
630 Message::RebalanceDryRun,
631 Message::RebalanceCompleted,
632 Message::DoctorHealthy,
633 Message::DoctorIssues,
634 Message::DoctorFixed,
635 Message::DoctorIssue,
636 Message::DoctorKindDanglingDependency,
637 Message::DoctorKindDanglingParent,
638 Message::DoctorKindDanglingSprint,
639 Message::DoctorKindParentCycle,
640 Message::DoctorKindDependencyCycle,
641 Message::DoctorKindDuplicateId,
642 Message::DoctorKindIssuedId,
643 Message::DoctorKindInvalidStatus,
644 Message::DoctorKindRankAnomaly,
645 Message::DoctorKindCollision,
646 Message::DoctorKindMalformedRecord,
647 Message::DoctorKindFilename,
648 Message::InvalidCapacityOptions,
649 Message::FailedToAction,
650 Message::ShellParseError,
651 Message::SprintAddRequiresItemOrStatus,
652 Message::ErrorInvalidItemId,
653 Message::ErrorInvalidRank,
654 Message::ErrorInvalidAutomationPlan,
655 Message::ErrorAutomationPlanSource,
656 Message::ErrorUnknownStatus,
657 Message::ErrorEmptyTitle,
658 Message::ErrorInvalidSearchPattern,
659 Message::ErrorInvalidFilterOption,
660 Message::ErrorNothingToUpdate,
661 Message::ErrorEmptyDod,
662 Message::ErrorInvalidTemplateName,
663 Message::ErrorTemplateNotFound,
664 Message::ErrorTemplateUnreadable,
665 Message::ErrorNotFound,
666 Message::ErrorReferencedItem,
667 Message::ErrorInvalidSprintId,
668 Message::ErrorInvalidSprintState,
669 Message::ErrorEmptySprintTitle,
670 Message::ErrorEmptySprintGoal,
671 Message::ErrorInvalidSprintTransition,
672 Message::ErrorSprintNotFound,
673 Message::ErrorSprintExists,
674 Message::ErrorSprintClosed,
675 Message::ErrorInvalidSprintPeriod,
676 Message::ErrorInvalidDailyWorkHours,
677 Message::ErrorInvalidDeductionFactor,
678 Message::ErrorInvalidSprintHolidays,
679 Message::ErrorSprintCapacityPeriodUnset,
680 Message::ErrorSprintCapacityUnset,
681 Message::ErrorSprintPeriodUnset,
682 Message::ErrorSprintEmpty,
683 Message::ErrorNotInSprint,
684 Message::ErrorParentCycle,
685 Message::ErrorNotInitialized,
686 Message::ErrorIo,
687 Message::ErrorParse,
688 Message::ErrorMissingFrontmatter,
689 Message::ErrorUnsupportedSqliteSchema,
690 Message::ErrorSelfReference,
691 Message::ErrorNotSibling,
692 Message::ErrorTask,
693 Message::ErrorGit,
694 Message::ErrorEditorNotSet,
695 Message::ErrorEditorLaunch,
696 Message::ErrorEditorInvalid,
697 Message::ErrorLocked,
698 Message::AlreadyInInteractiveShell,
699 Message::KanbanDetailsTitle,
700 Message::KanbanPopupHints,
701 Message::KanbanHelpTitle,
702 Message::KanbanHelpEntries,
703 Message::KanbanNoChanges,
704 Message::KanbanEditorFailed,
705 Message::KanbanEditFailed,
706 Message::KanbanWipExceeded,
707 Message::KanbanNoEditor,
708 Message::KanbanKeyHints,
709 Message::KanbanHelpHint,
710 Message::KanbanHelpClearFilter,
711 Message::KanbanAddTitlePrompt,
712 Message::KanbanAddBodyPrompt,
713 Message::KanbanAddParentPrompt,
714 Message::KanbanAddDependenciesPrompt,
715 Message::KanbanDependencyAddPrompt,
716 Message::KanbanDependencyRemovePrompt,
717 Message::KanbanEmptyTitle,
718 Message::KanbanEmptyDependency,
719 Message::KanbanDependencyAdded,
720 Message::KanbanDependencyRemoved,
721 Message::KanbanDependencyCycleWarning,
722 Message::KanbanParentPrompt,
723 Message::KanbanParentSet,
724 Message::KanbanParentCleared,
725 Message::KanbanActiveFilter,
726 Message::KanbanActiveRegexFilter,
727 Message::KanbanDependencyLegend,
728 Message::KanbanColumnRange,
729 Message::KanbanEmptyColumns,
730 Message::KanbanQuitBody,
731 Message::KanbanQuitPrompt,
732 Message::KanbanNoBody,
733 Message::KanbanNoSelection,
734 Message::KanbanTerminalInitFailed,
735 Message::EditGuidance,
736 Message::AutomationCommandValid,
737 Message::AutomationCommandInvalid,
738 Message::AutomationCommandFailed,
739 Message::AutomationCommandSkipped,
740 Message::AutomationDryRunCompleted,
741 Message::AutomationCompleted,
742 Message::AutomationPartialFailure,
743 Message::AutomationInvalidCommandArguments,
744 Message::AutomationNotExecutedAfterFailure,
745 Message::AutomationNotValidatedAfterFailure,
746 Message::AutomationDryRunGitInitFailed,
747 Message::AutomationDryRunWorkspaceUnavailable,
748 Message::AutomationCommandExited,
749 ];
750
751 for message in messages {
752 assert!(!localizer.text(message).is_empty());
753 }
754 assert!(
755 !localizer
756 .format(Message::Moved, [("id", "T-1"), ("status", "done")])
757 .is_empty()
758 );
759 assert!(localizer.lookup("missing-message").is_none());
760 }
761}