A command-line calendar utility similar to ncal, written in Scala. The name nods
to inox, Castilian for "stainless steel" - see Warranty.
- Display current month calendar
- Display specific month and year
- Display entire year calendar
- Leap year support
- Optional positional year argument
- Configurable starting weekday (
--starting-day 0..6) - Optional ISO week numbers (
-w,--week-numbers) - Optional TamboUI-colored output (
-c,--color) - Internationalization (i18n) support - Month names, day names, and error messages in multiple languages
- Clean, formatted output similar to traditional Unix cal/ncal utilities
- Configurable localization via command-line options
# Display current month
sbt run
# Display help
sbt "runMain inoxcal.InoxcalApp --help"
# Display specific month in current year (e.g., March)
sbt "runMain inoxcal.InoxcalApp --month 3"
# Display specific month and year (e.g., March 2024)
sbt "runMain inoxcal.InoxcalApp --month 3 --year 2024"
# Display entire current year
sbt "runMain inoxcal.InoxcalApp --year-view"
# Display specific year
sbt "runMain inoxcal.InoxcalApp --year-view --year 2024"
# Display a year using the positional argument
sbt "runMain inoxcal.InoxcalApp 2024"
# Start weeks on Monday and show ISO week numbers
sbt "runMain inoxcal.InoxcalApp --month 3 --year 2024 --starting-day 1 --week-numbers"
# Enable TamboUI-colored output when running in a real terminal
sbt "runMain inoxcal.InoxcalApp --month 3 --year 2024 --color"
# Display calendar in Spanish
sbt "runMain inoxcal.InoxcalApp --month 3 --year 2024 --locale es"
# Display calendar in French
sbt "runMain inoxcal.InoxcalApp --month 3 --year 2024 --locale fr"--helpor-h: Show usage information--year-viewor-y: Display entire year calendar[<year>]: Optional positional year; equivalent to--year <year>--month <int>: Specify month (1-12) to display--year <int>: Specify year to display--locale <string>or-l: Set locale for internationalization (e.g., 'es' for Spanish, 'fr' for French)--starting-day <int>: Set the first weekday, where Sunday is0and Saturday is6--week-numbersor-w: Display ISO week numbers for each calendar row--coloror-c: Enable TamboUI-styled colors when stdout is a real terminal
Notes:
--yearon its own prints the year view,--monthon its own uses the current year.- A positional year prints the year view. Supplying both a positional year and
--yearis rejected. --year-viewtakes precedence over--monthwhen both are given.- Accepted range: months
1-12and years1900-3000. Invalid values are reported on stderr and the process exits with status1. - Starting days must be in the range
0-6; day names and date columns rotate together. Week numbers use ISO week numbering. --coloruses TamboUI styles for headings, weekends, today, and week numbers. Color is automatically omitted when stdout is not attached to a real terminal.- The
Calendarclass itself is not limited to that range; the range is enforced by the command-line interface (seeCalendar.MinYear/MaxYear).
Display March 2024 in English (default):
March 2024
Su Mo Tu We Th Fr Sa
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Display March 2024 in Spanish:
Marzo 2024
Do Lu Ma Mi Ju Vi Sa
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Display March 2024 in French:
Mars 2024
Di Lu Ma Me Je Ve Sa
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Inoxcal supports multiple languages through Java's ResourceBundle system. The following languages are currently supported:
- English (en) - Default
- Spanish (es) - Español
- French (fr) - Français
Use the --locale or -l option to specify a language:
# English (default)
sbt "run --month 3 --year 2024"
# Spanish
sbt "run --month 3 --year 2024 --locale es"
# French
sbt "run --month 3 --year 2024 --locale fr"To add support for a new language:
- Create a new properties file in
src/main/resources/inoxcal/namedmessages_XX.propertieswhereXXis the language code - Translate all the keys from
messages.properties - The new locale is picked up automatically by the
--localeoption for month names, day names, error messages, and the help description/examples (defalls back to the built-in English bundle because nomessages_de.propertiesexists yet)
Two escaping rules matter when translating:
error.invalid.monthanderror.invalid.yearare rendered withjava.text.MessageFormat, so a literal apostrophe must be doubled (L''année)- the month, day, and help strings are used verbatim, so a single apostrophe is correct there (
toute l'année)
Example for German (messages_de.properties):
month.1=Januar
month.2=Februar
month.3=März
# ... etc
day.0=So
day.1=Mo
# ... etc
error.invalid.month=Ungültiger Monat: {0}. Der Monat muss zwischen {1} und {2} liegen.- Scala 3.8.4 (configured in
build.sbt, using modern significant indentation syntax) - JDK 17 or newer at runtime. JDK 17, 21, and 25 are tested in release CI.
- sbt 2.0.9 (pinned in
project/build.properties) for building from source
sbt compilesbt runsbt testsbt stageThis creates an executable script in target/universal/stage/bin/inoxcal.
Release archives are named inoxcal-<version>.zip and inoxcal-<version>.tar.gz.
Download either archive from the project's GitHub Release, extract it, and run:
./inoxcal-<version>/bin/inoxcal --help
./inoxcal-<version>/bin/inoxcal 2024The archive includes this README, the MIT license, the inoxcal launcher, and all
runtime dependencies. No Scala or sbt installation is needed, but Java 17 or newer
must be available on PATH (or configured through JAVA_HOME).
To build both archives locally:
sbt "Universal/packageBin ; Universal/packageZipTarball"The resulting files are written beneath target/universal/.
Create a release from an up-to-date, clean main checkout. The release process
creates the release commit and annotated tag locally; it does not push changes or
publish artifacts itself.
git switch main
git pull --ff-only origin main
git status --shortThe working tree should be clean before continuing. Run the complete local coverage check before creating a release:
sbt -batch "clean ; coverage ; test ; coverageReport"
test -s target/scala-3.8.4/scoverage-report/scoverage.xmlThe test command must run a nonzero number of tests and report that all tests passed. Coverage must meet the configured minimums of 90% statement coverage and 85% branch coverage.
Replace the example versions below with the next release and development versions:
sbt "release release-version 0.1.4 next-version 0.1.5-SNAPSHOT"This runs the configured sbt-release process, which checks snapshot dependencies,
cleans and tests the project, sets and commits the release version, creates the
annotated v0.1.4 tag, then commits the next development version. The tag points to
the release commit; main advances to the next snapshot commit.
Use explicit versions rather than blindly using release with-defaults when the
current version is already a released version. Explicit versions make it clear that
the release tag will be new rather than repeating an existing tag.
Inspect the result before pushing:
git log -3 --oneline --decorate
git show v0.1.4:version.sbt
git status --shortConfirm that the tag contains the release version and that the working tree is clean.
Push the branch and only the intended tag:
git push origin main
git push origin v0.1.4Avoid git push --tags, which could publish unrelated local tags.
Pushing the tag triggers the Tagged Release workflow. It validates the tag against
the sbt version, reruns tests and coverage, builds the ZIP and tar.gz archives,
verifies SHA-256 checksums, and smoke-tests both archives on Java 17, 21, and 25.
After those jobs pass, the workflow publishes the GitHub Release with the archives
and checksum file.
The release is complete only after the workflow succeeds and the GitHub Release is visible. If the workflow fails, inspect and fix the cause rather than moving or overwriting the published tag.
src/
├── main/
│ ├── scala/inoxcal/
│ │ ├── Calendar.scala # Core calendar logic
│ │ ├── CalendarTextRenderer.scala # Plain and TamboUI calendar rendering
│ │ ├── InoxcalApp.scala # Command-line interface
│ │ └── LocalizationManager.scala # Internationalization support
│ └── resources/inoxcal/
│ ├── messages.properties # English (default)
│ ├── messages_es.properties # Spanish
│ └── messages_fr.properties # French
└── test/scala/inoxcal/
├── CalendarTest.scala # Unit tests for the Calendar class
├── LocalizationManagerTest.scala # Tests for the resource bundles and i18n API
├── InoxcalAppTest.scala # Tests for the command-line interface
└── IntegrationTest.scala # Integration tests and exact output snapshots
Build files: build.sbt, scala.sbt (compiler options), project/plugins.sbt
and project/build.properties (pinned sbt version). CI runs
coverage test coverageReport on JDK 25; doc/ai/ holds the AI transcripts and
the remediation plan (doc/ai/20260930_RemediationPlan.md).
displayMonth(year, month): Display a specific monthdisplayMonth(year, month, startingDay, weekNumbers): Display a configured monthdisplayCurrentMonth(): Display current monthdisplayYear(year): Display entire yeardisplayYear(year, startingDay, weekNumbers): Display a configured yeardisplayCurrentYear(): Display current yeargetDaysInMonth(year, month): Get number of days in a monthisLeapYear(year): Check if a year is a leap yeargetDayOfWeek(year, month, day): Get day of week for a datemonthView(year, month, startingDay, weekNumbers): Build structured month data
The companion object exposes the range accepted by the command-line interface:
Calendar.MinYear/Calendar.MaxYear (1900-3000) and
Calendar.MinMonth/Calendar.MaxMonth (1-12). Starting-day values use
Calendar.MinStartingDay/Calendar.MaxStartingDay (0-6).
Calendar.withLocale(locale): Create calendar with specific Java LocaleCalendar.withLanguage(languageTag): Create calendar with specific language
getMonthName(month): Get localized month namegetDayName(dayOfWeek): Get localized day namegetInvalidMonthError(month): Get localized error message for invalid monthgetInvalidYearError(year): Get localized error message for invalid yeargetAllMonthNames/getAllDayNames: Get all names as an immutableIndexedSeqgetHelpDescription/getExamplesText: Get the localized help stringsgetCurrentLocale: Get the locale in useLocalizationManager.forLanguage(tag): Create manager for specific languageLocalizationManager.forLocale(language, country): Create manager for a specific locale
- Argument parsing and validation with mainargs
validateMonth/validateYearreturnEither[String, Unit], so the failure paths are unit testable- Invalid values are reported on stderr with exit status
1 - Localized help system (
--helpcombines the translated description and examples with the generated option list)
The project includes comprehensive tests:
- Unit Tests (
CalendarTest): individual methods and edge cases; every grid row must be exactly 20 characters wide - Localization Tests (
LocalizationManagerTest): bundle contents, locale fallback, and error messages for en/es/fr - Application Tests (
InoxcalAppTest): option parsing, validation failures throughEither, and end-to-end output captured withConsole.withOut - Integration Tests (
IntegrationTest): complete workflows plus exact ("golden") output snapshots
Tests run in a forked JVM with the locale pinned to en_US (see build.sbt), so
the output assertions do not depend on the machine running them.
Run all tests with:
sbt testCoverage is produced with a minimum statement-coverage gate (see build.sbt):
sbt coverage test coverageReport- Add functionality to the
Calendarclass - Update the
InoxcalAppobject for command-line interface changes - Add corresponding tests
- Update this README
The project follows modern Scala 3 conventions:
- Uses significant indentation (no curly braces)
- Use camelCase for methods and variables
- Use PascalCase for classes and objects
- Include comprehensive documentation
- Write tests for all new functionality
MIT - see LICENSE, © 2025 LUC COMP 371/471 Prog Language Course.
Guaranteed rust-free. Inox is short for inoxidable, Castilian for "stainless
steel": the word behind the "Inox" stamp on a kitchen sink and in acero
inoxidable. Inoxcal is therefore structurally stainless - it is written in Scala,
not Rust, so the only thing in this repository that can oxidize is the reader's
understanding of leap-year rules. There is no borrow checker, but you may borrow
any day of the month you like, as long as you give it back before midnight. The
MIT license covers everything except genuine corrosion: should your copy
develop rust, please file an issue with the detected oxide and we will prescribe
sbt clean compile.
This project was developed with assistance from generative and agentic AI tools, including Cline. AI assistance was used for brainstorming, implementation, documentation, and code review. Human contributors reviewed and tested the resulting work and remain responsible for the final contents of this repository.