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