diff --git a/cmd/skyfeed/main.go b/cmd/skyfeed/main.go index d7072d3..7185727 100644 --- a/cmd/skyfeed/main.go +++ b/cmd/skyfeed/main.go @@ -1,108 +1,143 @@ package main import ( - "fmt" - "os" - "strings" + "fmt" + "os" - "github.com/spf13/cobra" + "github.com/spf13/cobra" - "github.com/leaktechnologies/skyfeed/internal/config" - "github.com/leaktechnologies/skyfeed/internal/geo" - "github.com/leaktechnologies/skyfeed/internal/output" - "github.com/leaktechnologies/skyfeed/internal/weather" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/geo" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/output" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/weather" ) var rootCmd = &cobra.Command{ - Use: "skyfeed", - Short: "Skyfeed - Open Weather Engine for Telefact and Terminal", - Long: `Skyfeed fetches and normalizes weather data from Environment Canada, + Use: "skyfeed", + Short: "Skyfeed - Open Weather Engine for Telefact and Terminal", + Long: `Skyfeed fetches and normalizes weather data from Environment Canada, using a local IP database for accurate geolocation. It supports both CLI and API modes.`, - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, } func main() { - // Initialize configuration and ensure data directories exist - config.Init() + // Initialize configuration and ensure data directories exist + config.Init() - // Register subcommands - rootCmd.AddCommand(fetchCmd) - rootCmd.AddCommand(showCmd) - rootCmd.AddCommand(updateCmd) + // Register subcommands + rootCmd.AddCommand(fetchCmd) + rootCmd.AddCommand(showCmd) + rootCmd.AddCommand(updateCmd) + rootCmd.AddCommand(debugLogoCmd) - if err := rootCmd.Execute(); err != nil { - fmt.Println("Error:", err) - os.Exit(1) - } + if err := rootCmd.Execute(); err != nil { + fmt.Println("Error:", err) + os.Exit(1) + } } -// ------------------------------ +// ---------------------------------------------------- // Subcommands -// ------------------------------ +// ---------------------------------------------------- var fetchCmd = &cobra.Command{ - Use: "fetch", - Short: "Fetch the latest weather data for your current location", - Run: func(cmd *cobra.Command, args []string) { - output.LogInfo("Skyfeed: Checking IP database...") - if err := geo.EnsureIPDBUpToDate(); err != nil { - output.LogError(fmt.Sprintf("Failed to update IP DB: %v", err)) - return - } + Use: "fetch", + Short: "Fetch the latest weather data for your current location", + Run: func(cmd *cobra.Command, args []string) { + output.LogInfo("Skyfeed: Checking IP database...") + if err := geo.EnsureIPDBUpToDate(); err != nil { + output.LogError(fmt.Sprintf("Failed to update IP DB: %v", err)) + return + } - output.LogInfo("Skyfeed: Detecting location...") - city, lat, lon, err := geo.GetUserLocation() - if err != nil { - output.LogError(fmt.Sprintf("Could not determine location: %v", err)) - return - } - output.LogInfo(fmt.Sprintf("Detected: %s (%.4f, %.4f)", city, lat, lon)) + output.LogInfo("Skyfeed: Detecting location...") + city, lat, lon, err := geo.GetUserLocation() + if err != nil { + output.LogError(fmt.Sprintf("Could not determine location: %v", err)) + return + } + output.LogInfo(fmt.Sprintf("Detected: %s (%.4f, %.4f)", city, lat, lon)) - output.LogInfo("Finding nearest Environment Canada station...") - station, err := geo.FindNearestStation(lat, lon) - if err != nil { - output.LogError(fmt.Sprintf("Station lookup failed: %v", err)) - return - } - output.LogInfo(fmt.Sprintf("Nearest station: %s [%s]", station.Name, station.Code)) + output.LogInfo("Finding nearest Environment Canada station...") + station, err := geo.FindNearestStation(lat, lon) + if err != nil { + output.LogError(fmt.Sprintf("Station lookup failed: %v", err)) + return + } + output.LogInfo(fmt.Sprintf("Nearest station: %s [%s]", station.Name, station.Code)) - output.LogInfo("Fetching latest weather data...") - // Determine province from the station (if available) - province := strings.Split(station.Code, "_")[0] // fallback heuristic - data, err := weather.FetchCurrent(station.Code, province) - if err != nil { - output.LogError(fmt.Sprintf("Weather fetch failed: %v", err)) - return - } + output.LogInfo("Fetching latest weather data...") + province := station.Province - fmt.Println(output.FormatWeatherCLI(data, true)) - }, + data, err := weather.FetchCurrent(station.Code, province) + if err != nil { + output.LogError(fmt.Sprintf("Weather fetch failed: %v", err)) + return + } + + // Render dynamic header + output.RenderLogo(data.Condition) + fmt.Println() + + // Main output + fmt.Println(output.FormatWeatherCLI(data, true)) + }, } var showCmd = &cobra.Command{ - Use: "show", - Short: "Show cached weather data from disk", - Run: func(cmd *cobra.Command, args []string) { - data, err := weather.LoadCached() - if err != nil { - output.LogError(fmt.Sprintf("Failed to load cache: %v", err)) - return - } - fmt.Println(output.FormatWeatherCLI(data, true)) - }, + Use: "show", + Short: "Show cached weather data from disk", + Run: func(cmd *cobra.Command, args []string) { + data, err := weather.LoadCached() + if err != nil { + output.LogError(fmt.Sprintf("Failed to load cache: %v", err)) + return + } + + output.RenderLogo(data.Condition) + fmt.Println() + + fmt.Println(output.FormatWeatherCLI(data, true)) + }, } var updateCmd = &cobra.Command{ - Use: "update-ipdb", - Short: "Manually update the local IP geolocation database", - Run: func(cmd *cobra.Command, args []string) { - output.LogInfo("Forcing IP database update...") - if err := geo.ForceUpdateIPDB(); err != nil { - output.LogError(fmt.Sprintf("Update failed: %v", err)) - return - } - output.LogSuccess("IP database updated successfully.") - }, + Use: "update-ipdb", + Short: "Manually update the local IP geolocation database", + Run: func(cmd *cobra.Command, args []string) { + output.LogInfo("Forcing IP database update...") + if err := geo.ForceUpdateIPDB(); err != nil { + output.LogError(fmt.Sprintf("Update failed: %v", err)) + return + } + output.LogSuccess("IP database updated successfully.") + }, +} + +// ---------------------------------------------------- +// Debug-only utility command +// ---------------------------------------------------- + +var debugLogoCmd = &cobra.Command{ + Use: "debug-logo [condition]", + Hidden: true, + Short: "Render the dynamic ASCII Skyfeed logo for a given condition (debug use only)", + Long: `This command renders the Skyfeed ASCII logo using a simulated +weather condition. It is intended strictly for development and testing. +Example: + skyfeed debug-logo "Light Snow"`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Println("Usage: skyfeed debug-logo \"Weather Condition\"") + fmt.Println("Example: skyfeed debug-logo \"Sunny\"") + return + } + + condition := args[0] + fmt.Printf("Rendering logo for condition: %s\n\n", condition) + + output.RenderLogo(condition) + }, } diff --git a/config/default.json b/config/default.json index e69de29..6765c08 100644 --- a/config/default.json +++ b/config/default.json @@ -0,0 +1,9 @@ +{ + "language": "en-CA", + "location": { + "mode": "auto", + "manual_city": "", + "manual_lat": 0, + "manual_lon": 0 + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..f64cd9f --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,42 @@ +# Skyfeed Changelog +Format: vMAJOR.MINOR.PATCH[-devN|-betaN] +Branch: master + +## Unreleased +- Add upcoming changes here. + +## v0.1.0-dev1 — Initial Development Build +Tag: v0.1.0-dev1 +Date: 2025-11-12 + +Added: +- Initial project structure and base modules. +- MaxMind GeoLite2 IP geolocation integration. +- IP → coordinates geolocation pipeline. +- Town index and town lookup system. +- Weather fetching via Environment Canada citypageWeather XML. +- Weather normalization system. +- Terminal output formatter. +- Config loader and defaults. +- Scheduler stubs for future background updates. +- UI stubs (terminal and GUI, currently inactive). +- Station index placeholders (in-progress). + +Notes: +- First internal prototype. +- Many systems are incomplete or placeholders. +- Unstable behavior expected. + +# Versioning Rules +Development builds: +v0.1.0-dev1 +v0.1.0-dev2 +v0.1.0-dev3 + +Beta builds: +v0.1.0-beta1 +v0.1.0-beta2 + +Stable releases: +v0.1.0 +v0.2.0 diff --git a/docs/LANGUAGE_SUPPORT.md b/docs/LANGUAGE_SUPPORT.md new file mode 100644 index 0000000..93a7033 --- /dev/null +++ b/docs/LANGUAGE_SUPPORT.md @@ -0,0 +1,285 @@ +# Skyfeed Language and Localisation Support +Version v0.1.0-dev1 + +Skyfeed is a Canadian-focused weather engine and must provide native support for Canadian languages. This includes fully localised weather conditions, warnings, UI labels, and community/province names. The localisation system is shared with Telefact, so both systems display information consistently. + +This document describes the localisation model, supported language bundles, file structure, selection rules, and long-term objectives. + +------------------------------------------------------------ +1. Supported Locales +------------------------------------------------------------ + +Skyfeed currently supports the following Canadian languages: + +en-CA + Canadian English + Default system language + +fr-CA + Canadian French + Required for federal bilingual standards + Matches terminology used by Environment Canada and Radio-Canada + +iu-CA + Inuktitut + Written in syllabics + Primary language for Nunavut and northern communities + Provides localised place names and weather vocabulary + +Additional languages may be added in future releases. + +------------------------------------------------------------ +2. Locale Selection Rules +------------------------------------------------------------ + +Skyfeed selects a language using the following priority: + +1. If the user has configured a language in config/default.json, use it. +2. If the user specifies a language using a CLI flag, use it for that run. +3. On the first run, Skyfeed will prompt the user to choose a language. +4. If a translation key is missing for the chosen language, fall back to en-CA. + +Telefact inherits the same localisation engine and will display pages using the configured language. + +------------------------------------------------------------ +3. Localisation File Structure +------------------------------------------------------------ + +All localisation bundles and place-name dictionaries are stored in: + +internal/i18n/ + +Skyfeed uses the following files: + +internal/i18n/en_CA.json +internal/i18n/fr_CA.json +internal/i18n/iu_CA.json + +These contain: + locale code + language name + weather condition translations + weather alert translations + UI text + formatting rules + +Place name dictionaries are separate: + +internal/i18n/places_provinces.json +internal/i18n/places_nunavut.json +internal/i18n/places_cities.json (future) + +Example of a translation structure: + +{ + "locale": "en-CA", + "language_name": "English", + "conditions": { + "clear": "Clear", + "snow": "Snow", + "freezing_rain": "Freezing Rain" + }, + "alerts": { + "tornado_warning": "Tornado Warning", + "fog_advisory": "Fog Advisory" + }, + "ui": { + "loading": "Loading...", + "fetching_weather": "Fetching latest weather..." + } +} + +------------------------------------------------------------ +4. Inuktitut (iu-CA) Support +------------------------------------------------------------ + +Inuktitut support includes: + +1. Full syllabic script (ᐃᓄᒃᑎᑐᑦ) +2. Basic Roman orthography (future toggle) +3. Translation of all Nunavut community names +4. Translation of major northern place names in NWT (future) +5. Weather condition vocabulary +6. Weather alert vocabulary + +The goal is to provide a natural-feeling experience for northern users based on public Inuktitut terminology resources. + +Inuktitut keys that do not have translations will fall back to en-CA. + +------------------------------------------------------------ +5. Region-Based Default Language Logic +------------------------------------------------------------ + +Skyfeed may automatically choose a default language based on detected location: + +Nunavut + Default: iu-CA + Secondary: en-CA + +Quebec + Default: fr-CA + Secondary: en-CA + +Rest of Canada + Default: en-CA + +Users may override these defaults. + +------------------------------------------------------------ +6. First-Run Language Prompt +------------------------------------------------------------ + +On first run, the CLI or TUI will display: + +Please select your language +1. English (en-CA) +2. Français (fr-CA) +3. ᐃᓄᒃᑎᑐᑦ (iu-CA) + +The selected language will be saved in: + +~/.local/share/skyfeed/config.json +or +config/default.json + +------------------------------------------------------------ +7. Localised Weather Alerts +------------------------------------------------------------ + +All alert types defined in TAXONOMY.md must provide translations for: + +en-CA +fr-CA +iu-CA + +Example: + +tornado_warning + en-CA: Tornado Warning + fr-CA: Alerte Tornade + iu-CA: ᐊᓂᖅᑕᖅᑐᖅ ᐅᓪᓗᖓ + +If a translation is unavailable in iu-CA or fr-CA, Skyfeed will use the English version automatically. + +------------------------------------------------------------ +8. Localised Weather Conditions +------------------------------------------------------------ + +Normalised condition keys (see WEATHER_TAXONOMY.md) must be translated. + +Examples: + +clear + en-CA: Clear + fr-CA: Dégagé + iu-CA: ᐅᖃᓕᒫᓂᖅ + +snow + en-CA: Snow + fr-CA: Neige + iu-CA: ᐊᐳᑦ + +freezing_rain + en-CA: Freezing Rain + fr-CA: Pluie Verglaçante + iu-CA: ᓯᕗᓂᖅ ᐱᒋᐊᓂ + +------------------------------------------------------------ +9. Localised UI Terminology +------------------------------------------------------------ + +Examples: + +Fetching latest weather... + en-CA: Fetching weather data... + fr-CA: Récupération des données météo... + iu-CA: ᐊᐅᓪᓚᓂᖅ ᐅᑎᖃᑦᑕᖅ + +Current conditions + en-CA: Current Conditions + fr-CA: Conditions Actuelles + iu-CA: ᐅᓂᒃᑳᖅᑐᖅ + +Feels like + en-CA: Feels Like + fr-CA: Ressenti + iu-CA: ᐋᖅᑭᖅ ᐊᐅᓪᓚᓂᖅ + +------------------------------------------------------------ +10. Localised CLI Output +------------------------------------------------------------ + +Skyfeed will translate all textual output, including: + +City names +Province names +Weather descriptions +Wind and visibility data +Warnings and colour-coded alerts +Error and status messages + +Example French output: + +Cornwall, Ontario +14:05 EST +Ciel dégagé +Ressenti 22°C +Vent O 10 km/h +Avertissement de pluie + +------------------------------------------------------------ +11. UI and Telefact Integration +------------------------------------------------------------ + +Skyfeed GUI + Uses the same translation bundles + Respects user language preference + +Telefact + All pages, headings, alerts, and conditions will use the correct language bundle + Inuktitut news feeds will be supported as they become available + +------------------------------------------------------------ +12. Testing Localisation +------------------------------------------------------------ + +Tests must ensure: + +All missing keys fall back to en-CA +Invalid locale codes default to en-CA +iu-CA renders properly even with partial translations +Weather numerical formatting follows Canadian standards +Wind directions convert correctly per language + +Additionally, a debug command will allow testing all languages: + +skyfeed debug-languages --location "Iqaluit, NU" + +------------------------------------------------------------ +13. Future Language Expansion +------------------------------------------------------------ + +Future Canadian languages may include: + +Cree (cr-CA) +Ojibwe (oj-CA) +Innu (innu-CA) +Dene (den-CA) +Dakelh +Mohawk + +These will be added gradually as vocabulary sets become available. + +------------------------------------------------------------ +14. Limitations +------------------------------------------------------------ + +Some weather concepts may lack simple Inuktitut or Cree translations +Non-standard alerts may require English fallback +Some community names outside Nunavut may not have official Indigenous forms +Translation refinement from native speakers may be required + +Skyfeed must always remain functional even if translations are incomplete. + +------------------------------------------------------------ +End of LANGUAGE_SUPPORT.md diff --git a/docs/TAGGING.md b/docs/TAGGING.md new file mode 100644 index 0000000..1e97830 --- /dev/null +++ b/docs/TAGGING.md @@ -0,0 +1,26 @@ +# Skyfeed Tagging & Versioning Policy + +**Branch:** `master` (primary branch — no `main`) + +## Version Format +vMAJOR.MINOR.PATCH[-devN|-betaN] + +### Examples +- `v0.1.0-dev1` → first development build for v0.1.0 +- `v0.1.0-dev2` → second dev iteration +- `v0.1.0-beta1` → pre-release for testing +- `v0.1.0` → stable tagged release + +### Rules +- Increment **PATCH** for small fixes or cleanup. +- Increment **MINOR** for added features. +- Increment **MAJOR** for breaking changes or major re-architecture. +- Every push that meaningfully changes behavior should get a new `-devN` tag. +- Tags are always annotated (with `-a`), containing a descriptive changelog message. + +### Commands +```bash +git add . +git commit -m "Describe change" +git tag -a v0.1.0-dev2 -m "Second dev build: IP resolver refactor" +git push && git push origin v0.1.0-dev2 diff --git a/docs/TAXONOMY.md b/docs/TAXONOMY.md new file mode 100644 index 0000000..1b7a130 --- /dev/null +++ b/docs/TAXONOMY.md @@ -0,0 +1,388 @@ +[FILE: docs/TAXONOMY.md] + +# Skyfeed Weather Alert Taxonomy Canada +Version v0.1.0-dev1 +Scope Environment Canada MSC Alerts for Skyfeed and Telefact + +Skyfeed is a Canada-focused weather engine. +This taxonomy defines all alert types Skyfeed aims to support, based on + +Environment Canada Meteorological Service of Canada +Canadian severe weather and marine alerts +Air quality and wildfire smoke products + +The goal is to mirror every alert type that can realistically be issued for Canada, so that +Nothing important is missed +Alerts can be formatted and colour-coded cleanly +The taxonomy can be localised to en-CA, fr-CA, and iu-CA Inuktitut + +Note This file is taxonomy only. +Implementation details will live in internalweatheralerts.go and related files. + +------------------------------------------------------------ +1. Alert Levels and Concepts +------------------------------------------------------------ + +Skyfeed recognises these conceptual alert levels + +Warning +Watch +Advisory +Statement +Information or Special Weather + +Severity handling +1 Warnings +2 Watches +3 Advisories +4 Statements and informational products + +------------------------------------------------------------ +2. Core Severe Weather Warnings +------------------------------------------------------------ + +Tornado Warning +Severe Thunderstorm Warning +Snow Squall Warning +Squall Warning +Winter Storm Warning +Blizzard Warning +Snowfall Warning +Rainfall Warning +Freezing Rain Warning +Flash Freeze Warning +Wind Warning +Extreme Cold Warning +Heat Warning +Arctic Outflow Warning +Storm Surge Warning +Tropical Storm Warning +Hurricane Warning +Storm Warning Marine +Gale Warning Marine +Freezing Spray Warning Marine +High Water Level Warning + +------------------------------------------------------------ +3. Watches +------------------------------------------------------------ + +Tornado Watch +Severe Thunderstorm Watch +Snow Squall Watch +Special Weather Watch +Heat Watch +Cold Watch +Wind Watch +Hurricane Watch +Tropical Storm Watch +Storm Surge Watch + +------------------------------------------------------------ +4. Advisories +------------------------------------------------------------ + +Fog Advisory +Freezing Drizzle Advisory +Freezing Fog Advisory +Special Weather Advisory +Rainfall Advisory +Snowfall Advisory +Wind Advisory +Blowing Snow Advisory +Marine Fog Advisory +Freezing Spray Advisory + +------------------------------------------------------------ +5. Statements and Information Products +------------------------------------------------------------ + +Weather Statement +Special Weather Statement +Air Quality Statement +Smoke Statement +Dust Statement +Temperature Statement +Tropical Cyclone Information Statement +Outlook or Long Range Statement + +------------------------------------------------------------ +6. Air Quality and Wildfire Related Alerts +------------------------------------------------------------ + +Air Quality Advisory +Air Quality Statement +Wildfire Smoke Advisory +Wildfire Smoke Statement +Smoke Advisory +Dust and Smoke Statement + +------------------------------------------------------------ +7. Marine and Coastal Alerts +------------------------------------------------------------ + +Gale Warning +Storm Warning +Hurricane Force Wind Warning +Strong Wind Warning +Freezing Spray Warning +Squall Warning +Blizzard Warning Marine +High Water Level Warning +Storm Surge Warning +Wave or Surf Warning + +------------------------------------------------------------ +8. Arctic and Northern-Specific Alerts +------------------------------------------------------------ + +Arctic Outflow Warning +Extreme Cold Warning +Wind Chill Warning +Blowing Snow Advisory +Blizzard Warning +Freezing Spray Warning + +------------------------------------------------------------ +9. Rare but Possible Phenomena in Canada +------------------------------------------------------------ + +Funnel Cloud Advisory +Waterspout Warning +Tropical Cyclone Information Statement +Thunderstorm Outlook or Convective Outlook + +------------------------------------------------------------ +10. Out of Scope For Now +------------------------------------------------------------ + +Volcano Warning +Ashfall Warning +Lahar Warning +Lava Flow Warning +Tsunami Warning or Advisory +Radiation or Nuclear Incident Warning +Sandstorm or Haboob Alert + +If such an event appears in CAP data, Skyfeed will classify it under Other Alert. + +------------------------------------------------------------ +11. Internal Skyfeed Alert Codes Planned +------------------------------------------------------------ + +Skyfeed will define internal alert keys, for example + +tornado_warning +severe_thunderstorm_warning +snow_squall_warning +heat_warning +fog_advisory +air_quality_statement +arctic_outflow_warning +hurricane_watch +wildfire_smoke_advisory + +Each code will map to +English label +French label +Inuktitut label +Severity +Colour theme +Optional long description + +------------------------------------------------------------ +12. Language and Localisation Notes +------------------------------------------------------------ + +Supported languages +en-CA / Canadian English +fr-CA / Canadian French +iu-CA / Inuktitut + +------------------------------------------------------------ +13. Future Work +------------------------------------------------------------ + +CAP to Skyfeed mapping layer +Colour and icon system +Alert grouping rules +Multi-language rendering +Per-page prioritisation for Telefact + +END OF FILE + + +[FILE: docs/TAXONOMY_WEATHER.md] + +# Skyfeed Weather Condition Taxonomy +Version v0.1.0-dev1 +Scope Normalised condition codes for Canada for Skyfeed and Telefact + +This document defines all base weather conditions, grouped logically, and provides stable internal keys for Skyfeed. +Translations will be handled separately. + +------------------------------------------------------------ +1. Skyfeed Condition Structure +------------------------------------------------------------ + +Each condition has +A unique internal key +A short description +A category +A suggested icon +A colour theme mapping + +Categories are +Sky conditions +Precipitation +Visibility +Wind +Arctic or cold-specific +Thunderstorm and convection +Smoke and air quality +Marine + +------------------------------------------------------------ +2. Sky Conditions +------------------------------------------------------------ + +clear +mainly_clear +partly_cloudy +mostly_cloudy +cloudy +overcast +sunny_breaks +cloudy_with_sunny_breaks + +------------------------------------------------------------ +3. Precipitation Types +------------------------------------------------------------ + +drizzle +light_drizzle +freezing_drizzle +rain +light_rain +heavy_rain +freezing_rain +rain_showers +snow +light_snow +heavy_snow +ice_pellets +sleet +mixed_precipitation +snow_showers +rain_and_snow +hail +graupel + +------------------------------------------------------------ +4. Visibility and Obscuration +------------------------------------------------------------ + +fog +freezing_fog +mist +haze +smoke +blowing_snow +drifting_snow +dust +sand +ice_crystals + +------------------------------------------------------------ +5. Wind and Motion +------------------------------------------------------------ + +wind_light +wind_moderate +wind_strong +wind_gale +wind_storm +wind_hurricane_force +gusty_winds + +------------------------------------------------------------ +6. Thunderstorm and Convective Weather +------------------------------------------------------------ + +thunderstorm +strong_thunderstorm +severe_thunderstorm +thundershowers +lightning_only + +------------------------------------------------------------ +7. Arctic and Northern Conditions +------------------------------------------------------------ + +extreme_cold +wind_chill +arctic_outflow +ice_fog +polar_low +freezing_spray +sea_ice +blizzard_conditions + +------------------------------------------------------------ +8. Marine Weather Conditions +------------------------------------------------------------ + +marine_fog +marine_freezing_spray +rough_seas +high_waves +storm_surge_conditions +water_spout + +------------------------------------------------------------ +9. Special and Rare Conditions +------------------------------------------------------------ + +funnel_cloud +volcanic_ash +ash_in_air +dust_storm +smoke_storm + +------------------------------------------------------------ +10. Normalisation Table Notes +------------------------------------------------------------ + +Environment Canada condition strings must be mapped into the above keys. +Examples + +Mainly Sunny maps to mainly_clear +A Few Clouds maps to partly_cloudy +Periods of Rain maps to rain +Risk of Freezing Rain maps to freezing_rain +Snow at times heavy maps to heavy_snow +Ice Crystals maps to ice_crystals +Local Smoke maps to smoke +Haze maps to haze +Mist maps to mist +Thunderstorms maps to thunderstorm unless severity tag present + +A complete mapping list will be implemented in internalweathernormalize.go. + +------------------------------------------------------------ +11. Colour Theme Notes +------------------------------------------------------------ + +clear primary sky blue and secondary gold +cloudy light grey to blue-grey +rain slate blue +snow white to light grey +fog mid grey +smoke brown-grey +thunderstorm violet purple accents +arctic cold pale blue and white +marine deep blue + +These themes apply to the Skyfeed logo. + +END OF FILE diff --git a/go.mod b/go.mod index 46009be..b66e9ca 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/leaktechnologies/skyfeed +module git.leaktechnologies.dev/Leak_Technologies/Skyfeed go 1.25.4 diff --git a/internal/geo/geolocate.go b/internal/geo/geolocate.go index d65f603..6a1f3e0 100644 --- a/internal/geo/geolocate.go +++ b/internal/geo/geolocate.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" "github.com/oschwald/geoip2-golang" ) diff --git a/internal/geo/ipdb_updater.go b/internal/geo/ipdb_updater.go index 953df74..da67cb9 100644 --- a/internal/geo/ipdb_updater.go +++ b/internal/geo/ipdb_updater.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) const ( diff --git a/internal/geo/stations.go b/internal/geo/stations.go index 64a6324..84e1d53 100644 --- a/internal/geo/stations.go +++ b/internal/geo/stations.go @@ -12,7 +12,7 @@ import ( "path/filepath" "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) // Station represents an Environment Canada citypage station. diff --git a/internal/geo/towns_lookup.go b/internal/geo/towns_lookup.go index 533534e..f4c342b 100644 --- a/internal/geo/towns_lookup.go +++ b/internal/geo/towns_lookup.go @@ -7,7 +7,7 @@ import ( "os" "path/filepath" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) // Town represents a Canadian town record (loaded from towns.json). diff --git a/internal/geo/towns_updater.go b/internal/geo/towns_updater.go index 036f739..4d603e0 100644 --- a/internal/geo/towns_updater.go +++ b/internal/geo/towns_updater.go @@ -9,7 +9,7 @@ import ( "path/filepath" "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) const ( diff --git a/internal/locale/en_CA.go b/internal/locale/en_CA.go new file mode 100644 index 0000000..c190aa4 --- /dev/null +++ b/internal/locale/en_CA.go @@ -0,0 +1,5 @@ +package locale + +func init() { + // Loaded automatically from en_CA.json via LoadAll() +} diff --git a/internal/locale/fr_CA.go b/internal/locale/fr_CA.go new file mode 100644 index 0000000..a746e96 --- /dev/null +++ b/internal/locale/fr_CA.go @@ -0,0 +1,5 @@ +package locale + +func init() { + // Loaded automatically from fr_CA.json +} diff --git a/internal/locale/i18n/en_CA.json b/internal/locale/i18n/en_CA.json new file mode 100644 index 0000000..95b0703 --- /dev/null +++ b/internal/locale/i18n/en_CA.json @@ -0,0 +1,131 @@ +{ + "locale": "en-CA", + "language_name": "English", + "script": "latin", + + "ui": { + "loading": "Loading...", + "fetching_weather": "Fetching weather data...", + "current_conditions": "Current Conditions", + "feels_like": "Feels Like", + "humidity": "Humidity", + "wind": "Wind", + "pressure": "Pressure", + "visibility": "Visibility", + "sunrise": "Sunrise", + "sunset": "Sunset", + "alerts": "Alerts", + "no_alerts": "No active alerts", + "updated": "Updated", + "location": "Location", + "language": "Language", + "select_language": "Select your language", + "select_location": "Select your location", + "manual_entry": "Enter Manually" + }, + + "conditions": { + "clear": "Clear", + "mainly_clear": "Mainly Clear", + "partly_cloudy": "Partly Cloudy", + "mostly_cloudy": "Mostly Cloudy", + "cloudy": "Cloudy", + "overcast": "Overcast", + + "rain": "Rain", + "light_rain": "Light Rain", + "moderate_rain": "Moderate Rain", + "heavy_rain": "Heavy Rain", + "freezing_rain": "Freezing Rain", + "drizzle": "Drizzle", + "freezing_drizzle": "Freezing Drizzle", + + "snow": "Snow", + "light_snow": "Light Snow", + "moderate_snow": "Moderate Snow", + "heavy_snow": "Heavy Snow", + "snow_grains": "Snow Grains", + "blowing_snow": "Blowing Snow", + "ice_pellets": "Ice Pellets", + "hail": "Hail", + + "fog": "Fog", + "dense_fog": "Dense Fog", + "freezing_fog": "Freezing Fog", + "mist": "Mist", + + "thunderstorm": "Thunderstorm", + "thunderstorm_rain": "Thunderstorm with Rain", + "thunderstorm_snow": "Thunderstorm with Snow", + + "smoke": "Smoke", + "haze": "Haze", + "dust": "Dust", + "blowing_dust": "Blowing Dust", + "ash": "Ash", + + "funnel_cloud": "Funnel Cloud", + "waterspout": "Waterspout" + }, + + "alerts": { + "tornado_warning": "Tornado Warning", + "tornado_watch": "Tornado Watch", + + "severe_thunderstorm_warning": "Severe Thunderstorm Warning", + "severe_thunderstorm_watch": "Severe Thunderstorm Watch", + + "snow_squall_warning": "Snow Squall Warning", + "snow_squall_watch": "Snow Squall Watch", + + "winter_storm_warning": "Winter Storm Warning", + "blizzard_warning": "Blizzard Warning", + "snowfall_warning": "Snowfall Warning", + "rainfall_warning": "Rainfall Warning", + "flash_freeze_warning": "Flash Freeze Warning", + "freezing_rain_warning": "Freezing Rain Warning", + + "wind_warning": "Wind Warning", + "wind_watch": "Wind Watch", + + "heat_warning": "Heat Warning", + "cold_warning": "Cold Warning", + "extreme_cold_warning": "Extreme Cold Warning", + "arctic_outflow_warning": "Arctic Outflow Warning", + "wind_chill_warning": "Wind Chill Warning", + + "fog_advisory": "Fog Advisory", + "freezing_fog_advisory": "Freezing Fog Advisory", + "freezing_drizzle_advisory": "Freezing Drizzle Advisory", + "blowing_snow_advisory": "Blowing Snow Advisory", + + "air_quality_statement": "Air Quality Statement", + "air_quality_advisory": "Air Quality Advisory", + + "smoke_statement": "Smoke Statement", + "smoke_advisory": "Smoke Advisory", + "wildfire_smoke_advisory": "Wildfire Smoke Advisory", + + "dust_statement": "Dust Statement", + "blowing_dust_advisory": "Blowing Dust Advisory", + + "special_weather_statement": "Special Weather Statement", + "weather_statement": "Weather Statement", + + "marine_freezing_spray_warning": "Freezing Spray Warning", + "marine_gale_warning": "Gale Warning", + "marine_storm_warning": "Storm Warning", + "marine_hurricane_force_warning": "Hurricane Force Wind Warning", + "marine_strong_wind_warning": "Strong Wind Warning", + + "tropical_storm_warning": "Tropical Storm Warning", + "tropical_storm_watch": "Tropical Storm Watch", + "hurricane_warning": "Hurricane Warning", + "hurricane_watch": "Hurricane Watch", + "storm_surge_warning": "Storm Surge Warning", + "storm_surge_watch": "Storm Surge Watch", + + "funnel_cloud_advisory": "Funnel Cloud Advisory", + "waterspout_warning": "Waterspout Warning" + } +} diff --git a/internal/locale/i18n/fr_CA.json b/internal/locale/i18n/fr_CA.json new file mode 100644 index 0000000..742acf6 --- /dev/null +++ b/internal/locale/i18n/fr_CA.json @@ -0,0 +1,131 @@ +{ + "locale": "fr-CA", + "language_name": "Français", + "script": "latin", + + "ui": { + "loading": "Chargement...", + "fetching_weather": "Récupération des données météo...", + "current_conditions": "Conditions actuelles", + "feels_like": "Ressenti", + "humidity": "Humidité", + "wind": "Vent", + "pressure": "Pression", + "visibility": "Visibilité", + "sunrise": "Lever du soleil", + "sunset": "Coucher du soleil", + "alerts": "Alertes", + "no_alerts": "Aucune alerte active", + "updated": "Mis à jour", + "location": "Emplacement", + "language": "Langue", + "select_language": "Choisissez votre langue", + "select_location": "Choisissez votre emplacement", + "manual_entry": "Entrée manuelle" + }, + + "conditions": { + "clear": "Dégagé", + "mainly_clear": "Généralement dégagé", + "partly_cloudy": "Partiellement nuageux", + "mostly_cloudy": "Généralement nuageux", + "cloudy": "Nuageux", + "overcast": "Couvert", + + "rain": "Pluie", + "light_rain": "Pluie faible", + "moderate_rain": "Pluie modérée", + "heavy_rain": "Pluie forte", + "freezing_rain": "Pluie verglaçante", + "drizzle": "Bruine", + "freezing_drizzle": "Bruine verglaçante", + + "snow": "Neige", + "light_snow": "Neige faible", + "moderate_snow": "Neige modérée", + "heavy_snow": "Neige forte", + "snow_grains": "Grains de neige", + "blowing_snow": "Poudrerie", + "ice_pellets": "Granules de glace", + "hail": "Grêle", + + "fog": "Brouillard", + "dense_fog": "Brouillard dense", + "freezing_fog": "Brouillard givrant", + "mist": "Brume", + + "thunderstorm": "Orage", + "thunderstorm_rain": "Orage avec pluie", + "thunderstorm_snow": "Orage avec neige", + + "smoke": "Fumée", + "haze": "Brume sèche", + "dust": "Poussière", + "blowing_dust": "Poussière soulevée", + "ash": "Cendre", + + "funnel_cloud": "Nuage en entonnoir", + "waterspout": "Trombe marine" + }, + + "alerts": { + "tornado_warning": "Alerte de tornade", + "tornado_watch": "Veille de tornade", + + "severe_thunderstorm_warning": "Alerte d’orage violent", + "severe_thunderstorm_watch": "Veille d’orage violent", + + "snow_squall_warning": "Alerte de rafales de neige", + "snow_squall_watch": "Veille de rafales de neige", + + "winter_storm_warning": "Alerte de tempête hivernale", + "blizzard_warning": "Alerte de blizzard", + "snowfall_warning": "Alerte de neige", + "rainfall_warning": "Alerte de pluie", + "flash_freeze_warning": "Alerte de gel éclair", + "freezing_rain_warning": "Alerte de pluie verglaçante", + + "wind_warning": "Alerte de vents forts", + "wind_watch": "Veille de vents forts", + + "heat_warning": "Alerte de chaleur", + "cold_warning": "Alerte de froid", + "extreme_cold_warning": "Alerte de froid extrême", + "arctic_outflow_warning": "Alerte d’écoulement arctique", + "wind_chill_warning": "Alerte de refroidissement éolien", + + "fog_advisory": "Avis de brouillard", + "freezing_fog_advisory": "Avis de brouillard givrant", + "freezing_drizzle_advisory": "Avis de bruine verglaçante", + "blowing_snow_advisory": "Avis de poudrerie", + + "air_quality_statement": "Déclaration sur la qualité de l’air", + "air_quality_advisory": "Avis sur la qualité de l’air", + + "smoke_statement": "Déclaration de fumée", + "smoke_advisory": "Avis de fumée", + "wildfire_smoke_advisory": "Avis de fumée de feux de forêt", + + "dust_statement": "Déclaration de poussière", + "blowing_dust_advisory": "Avis de poussière soulevée", + + "special_weather_statement": "Déclaration météorologique spéciale", + "weather_statement": "Déclaration météo", + + "marine_freezing_spray_warning": "Avertissement de givre marin", + "marine_gale_warning": "Avertissement de coup de vent", + "marine_storm_warning": "Avertissement de tempête", + "marine_hurricane_force_warning": "Avertissement de vents de force ouragan", + "marine_strong_wind_warning": "Avertissement de vents forts", + + "tropical_storm_warning": "Avertissement de tempête tropicale", + "tropical_storm_watch": "Veille de tempête tropicale", + "hurricane_warning": "Avertissement d’ouragan", + "hurricane_watch": "Veille d’ouragan", + "storm_surge_warning": "Avertissement de marée de tempête", + "storm_surge_watch": "Veille de marée de tempête", + + "funnel_cloud_advisory": "Avis de nuage en entonnoir", + "waterspout_warning": "Avertissement de trombe marine" + } +} diff --git a/internal/locale/i18n/iu_CA.json b/internal/locale/i18n/iu_CA.json new file mode 100644 index 0000000..dbe9eb5 --- /dev/null +++ b/internal/locale/i18n/iu_CA.json @@ -0,0 +1,125 @@ +{ + "locale": "iu-CA", + "language_name": "ᐃᓄᒃᑎᑐᑦ (Inuktitut)", + "script": "syllabics_with_roman", + + "ui": { + "loading": "ᐊᐅᓚᑦᑕᖅ... (aulattaq)", + "fetching_weather": "ᐅᑎᖃᑦᑕᖅ ᐊᐃᑉᐳᖅ... (utiqattaq aippuq)", + "current_conditions": "ᐅᓂᒃᑳᖅᑐᖅ (unikkaaqtuk)", + "feels_like": "ᐋᖅᑭᖅ (aaqiq)", + "humidity": "ᐃᒡᓗᒃᑕᐅᓂᖅ (igluktauq)", + "wind": "ᐊᑕᐅᓯᕐᓂᖅ (atausirniq)", + "pressure": "ᐅᒥᐊᕆᓂᖅ (umiarniq)", + "visibility": "ᐅᓪᓗᖅᑐᖅ (ullurqtuk)", + "sunrise": "ᐅᐳᓗᐃᑦ (upuluit)", + "sunset": "ᐊᖅᑕᐅᓯᖅ (aqtausiq)", + "alerts": "ᐅᖃᐅᓯᓕᖅᑯᑦ (uqausiliqkut)", + "no_alerts": "ᐅᖃᐅᓯᓕᖅᑯᑦ ᐱᑕᖃᙱᑦᑐᑦ (uqausiliqkut pitagaqngittut)", + "updated": "ᓄᓇᕗᑦ (nunavut)", + "location": "ᐃᓕᓴᐃᔭᖅ (ilisajaaq)", + "select_language": "ᐅᖃᐅᓯᖅ ᐅᖃᐅᓯᕐᓂᖅ (uqausiq uqaunirq)", + "select_location": "ᐃᓕᓴᐃᔭᖅ ᐅᖃᐅᓯᖅ (ilisajaaq uqausiq)", + "manual_entry": "ᐅᖃᐅᓯᓂᖅ ᐊᑐᖅᑳᖅ (uqausiniq atukkaa)" + }, + + "conditions": { + "clear": "ᐅᖃᓕᒫᓂᖅ (uqarliiq)", + "mainly_clear": "ᐅᖃᓕᒫᓂᖅ ᐊᒻᒪ ᑕᒻᒪᖁᑎᖅ (uqarliiq amma tammaqutiq)", + "partly_cloudy": "ᐋᖅᑐᖅ ᐃᕐᖃᑕᐅᓯᖅ (aaktuq irqatutsi)", + "mostly_cloudy": "ᐃᕐᖃᑕᐅᓯᖅ ᐊᒻᒪ ᐅᖃᓕᒫᓂᖅ (irqatutsi amma uqarliiq)", + "cloudy": "ᐃᕐᖃᑕᐅᓯᖅ (irqatutsi)", + "overcast": "ᐃᕐᖃᑕᐅᓯᖅ ᐅᓐᓂᐊᕐᓗᒋᑦ (irqatutsi unniarlugit)", + + "rain": "ᒪᓚᐅᑦ (malaut)", + "light_rain": "ᒪᓚᐅᑦ ᓇᐅᔭᖅ (malaut nauyaq)", + "moderate_rain": "ᒪᓚᐅᑦ ᐅᓐᓂᐊᕆᐊᖅ (malautunniariak)", + "heavy_rain": "ᒪᓚᐅᑦ ᐱᔪᓐᓇᕐᓗᒍ (malaut pijunnarluq)", + "freezing_rain": "ᓯᕗᓂᖅ ᒪᓚᐅᑦ (sivuniq malaut)", + "drizzle": "ᐃᓄᐃᑕᐅᖅ ᒪᓚᐅᑦ (inuittauq malaut)", + "freezing_drizzle": "ᓯᕗᓂᖅ ᐃᓄᐃᑕᐅᖅ (sivuniq inuittauq)", + + "snow": "ᐊᐳᑦ (aput)", + "light_snow": "ᐊᐳᑦ ᓇᐅᔭᖅ (aput nauyaq)", + "moderate_snow": "ᐊᐳᑦ ᐅᓐᓂᐊᕆᐊᖅ (aputunniariak)", + "heavy_snow": "ᐊᐳᑦ ᐱᔪᓐᓇᕐᓗᒍ (aput pijunnarluq)", + "snow_grains": "ᐊᐳᑦ ᓄᓇᓱᒃ (aput nunasuk)", + "blowing_snow": "ᐊᐳᑦ ᐊᑕᐅᓯᕐᓗᒍ (aput ataussirluk)", + "ice_pellets": "ᓯᕗᓂᖅ ᓄᓇᓱᒃ (sivuniq nunasuk)", + "hail": "ᐱᒋᐊᓯᐅᖅ (pijasiuq)", + + "fog": "ᐃᒡᓗᒃᑕᐅᓯᖅ (igluktauqsik)", + "dense_fog": "ᐃᒡᓗᒃᑕᐅᓯᖅ ᐱᔪᓐᓇᖅ (igluktauqsik pijunnaq)", + "freezing_fog": "ᓯᕗᓂᖅ ᐃᒡᓗᒃᑕᐅᓯᖅ (sivuniq igluktauqsik)", + "mist": "ᐃᒡᓗᒃᑕᐅᐳᖅ (igluktaupuk)", + + "thunderstorm": "ᒪᒃᑯᖅ (makkuq)", + "thunderstorm_rain": "ᒪᒃᑯᖅ ᒪᓚᐅᑦ (makkuq malaut)", + "thunderstorm_snow": "ᒪᒃᑯᖅ ᐊᐳᑦ (makkuq aput)", + + "smoke": "ᐊᐅᓐᓇᓯᖅ (aunnasiq)", + "haze": "ᐅᐳᓐᓂᐊᕆᐊᖅ ᐊᐅᓐᓇᓯᖅ (upunniariaq aunnasiq)", + "dust": "ᐊᓕᐅᑲᖅ (aliuqaq)", + "blowing_dust": "ᐊᓕᐅᑲᖅ ᐊᑕᐅᓯᕐᓗᒍ (aliuqaq ataussirluk)", + "ash": "ᐋᔾᔪ (aajju)", + + "funnel_cloud": "ᐅᖃᕐᖅᑐᓗᒋᑦ ᐃᒡᓗᒃ (uqarrtulugiqluk)", + "waterspout": "ᐅᖃᕐᖅᑐᓗᒋᑦ ᐊᕐᓇᑐᖅ (uqarrtulugiqarnaqtuk)" + }, + + "alerts": { + "tornado_warning": "ᐊᓂᖅᑕᖅᑐᖅ ᐅᓪᓗᖓ (aniqtaktuk ulluq)", + "tornado_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐅᓪᓗᖓ (titiraqanngittuq ulluq)", + + "severe_thunderstorm_warning": "ᐱᔪᓐᓇᕐᓗᒍ ᒪᒃᑯᖅ ᐊᓂᖅᑕᖅᑐᖅ (pijunnarluq makkuq)", + "severe_thunderstorm_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐱᔪᓐᓇᕐᓗᒍ ᒪᒃᑯᖅ (titiraqanngittuq makkuq)", + + "snow_squall_warning": "ᐊᐳᑦ ᐊᑕᐅᓯᕐᓗᒍ ᐊᓂᖅᑕᖅᑐᖅ (aput ataussirluk)", + "snow_squall_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐊᐳᑦ ᐊᑕᐅᓯᕐᓗᒍ (titiraqanngittuq aput)", + + "winter_storm_warning": "ᐊᐳᑦ ᐱᔪᓐᓇᕐᓗᒍ ᐊᓂᖅᑕᖅᑐᖅ (aput pijunnarluq)", + "blizzard_warning": "ᐱᔪᓐᓇᕐᓗᒍ ᐊᓂᖅᑕᖅᑐᖅ (pijunnarluq aput)", + + "rainfall_warning": "ᒪᓚᐅᑦ ᐊᓂᖅᑕᖅᑐᖅ (malaut aniqtaktuk)", + "snowfall_warning": "ᐊᐳᑦ ᐊᓂᖅᑕᖅᑐᖅ (aput aniqtaktuk)", + "wind_warning": "ᐊᑕᐅᓯᕐᓂᖅ ᐊᓂᖅᑕᖅᑐᖅ (atausirniq aniqtaktuk)", + "wind_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐊᑕᐅᓯᕐᓂᖅ (titiraqanngittuq atausirniq)", + + "heat_warning": "ᐅᒥᐊᕆᓂᖅ ᐊᓂᖅᑕᖅᑐᖅ (umiarniq aniqtaktuk)", + "cold_warning": "ᐊᖅᑕᐅᔾᔨᖅᑕᖅᑐᖅ (aqtaujjirqtuk)", + "extreme_cold_warning": "ᐱᔪᓐᓇᕐᓗᒍ ᐊᖅᑕᐅᔾᔨᖅᑕᖅᑐᖅ (pijunnarluq aqtaujjirqtuk)", + "arctic_outflow_warning": "ᐊᓗᒃᑕᖅ ᐊᑕᐅᓯᕐᓗᒍ (aluktaq ataussirluk)", + "wind_chill_warning": "ᐊᑕᐅᓯᕐᓗᒍ ᐊᖅᑕᐅᔾᔨᖅᑕᖅᑐᖅ (atausirniq aqtaujjirqtuk)", + + "flash_freeze_warning": "ᓯᕗᓂᖅ ᐱᔪᓐᓇᕐᓗᒍ (sivuniq pijunnarluq)", + "freezing_rain_warning": "ᓯᕗᓂᖅ ᒪᓚᐅᑦ (sivuniq malaut)", + + "fog_advisory": "ᐃᒡᓗᒃᑕᐅᓯᖅ ᑎᑎᖅᑲᓐᓂᖅ (igluktauqsik titiraqanngittuq)", + "freezing_fog_advisory": "ᓯᕗᓂᖅ ᐃᒡᓗᒃᑕᐅᓯᖅ ᑎᑎᖅᑲᓐᓂᖅ (sivuniq igluktauqsik titiraqanngittuq)", + "blowing_snow_advisory": "ᐊᐳᑦ ᐊᑕᐅᓯᕐᓂᖅ ᑎᑎᖅᑲᓐᓂᖅ (aput ataussirluk titiraqanngittuq)", + + "air_quality_statement": "ᐃᒡᓗᒃᑕᐅᓯᖅ ᐱᖁᓂᖅ (igluktauq pinniq)", + "air_quality_advisory": "ᐃᒡᓗᒃᑕᐅᓯᖅ ᑎᑎᖅᑲᓐᓂᖅ (igluktauq titiraqanngittuq)", + + "smoke_statement": "ᐊᐅᓐᓇᓯᖅ ᐱᖁᓂᖅ (aunnasiq pinniq)", + "smoke_advisory": "ᐊᐅᓐᓇᓯᖅ ᑎᑎᖅᑲᓐᓂᖅ (aunnasiq titiraqanngittuq)", + + "special_weather_statement": "ᐊᐃᑉᐳᖅ ᐱᖁᓂᖅ (aippuq pinniq)", + + "marine_freezing_spray_warning": "ᓯᕗᓂᖅ ᐊᕐᓇᑐᖅ ᐊᓂᖅᑕᖅᑐᖅ (sivuniq arnatuk)", + "marine_gale_warning": "ᐊᑕᐅᓯᕐᓗᒍ ᐊᓂᖅᑕᖅᑐᖅ (atausirniq aniqtaktuk)", + "marine_storm_warning": "ᐊᐃᑉᐳᖅ ᐊᓂᖅᑕᖅᑐᖅ (aippuq aniqtaktuk)", + "marine_hurricane_force_warning": "ᐅᐳᓐᓂᐊᕆᐊᖅ ᐊᓂᖅᑕᖅᑐᖅ (upunniariaq aniqtaktuk)", + + "tropical_storm_warning": "ᐊᐃᑉᐳᖅ ᐊᓂᖅᑕᖅᑐᖅ (aippuq aniqtaktuk)", + "tropical_storm_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐊᐃᑉᐳᖅ (titiraqanngittuq aippuq)", + "hurricane_warning": "ᐅᐳᓐᓂᐊᕆᐊᖅ ᐊᓂᖅᑕᖅᑐᖅ (upunniariaq aniqtaktuk)", + "hurricane_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐅᐳᓐᓂᐊᕆᐊᖅ (titiraqanngittuq upunniariaq)", + + "storm_surge_warning": "ᐊᐃᑉᐳᖅ ᐋᕐᓇᑐᖅ ᐊᓂᖅᑕᖅᑐᖅ (aippuq aarnatuk)", + "storm_surge_watch": "ᑎᑎᖅᑲᓐᓂᖅ ᐊᐃᑉᐳᖅ ᐋᕐᓇᑐᖅ (titiraqanngittuq aarnatuk)", + + "funnel_cloud_advisory": "ᐅᖃᕐᖅᑐᓗᒋᑦ ᐃᒡᓗᒃ ᑎᑎᖅᑲᓐᓂᖅ (uqarrtulugiqluk titiraqanngittuq)", + "waterspout_warning": "ᐅᖃᕐᖅᑐᓗᒋᑦ ᐊᕐᓇᑐᖅ ᐊᓂᖅᑕᖅᑐᖅ (uqarrtulugiqarnaqtuk)" + } +} diff --git a/internal/locale/iu_CA.go b/internal/locale/iu_CA.go new file mode 100644 index 0000000..5bca3ec --- /dev/null +++ b/internal/locale/iu_CA.go @@ -0,0 +1,5 @@ +package locale + +func init() { + // Loaded automatically from iu_CA.json +} diff --git a/internal/locale/loader.go b/internal/locale/loader.go new file mode 100644 index 0000000..f8169eb --- /dev/null +++ b/internal/locale/loader.go @@ -0,0 +1,41 @@ +package locale + +import ( + "embed" + "encoding/json" + "fmt" + "path/filepath" +) + +// Embed all locale JSON files inside binary +//go:embed i18n/*.json +var localeFS embed.FS + +// Load all languages on startup +func LoadAll() error { + files, err := localeFS.ReadDir("i18n") + if err != nil { + return fmt.Errorf("locale: cannot read embedded FS: %w", err) + } + + for _, f := range files { + name := f.Name() + path := filepath.Join("i18n", name) + + data, err := localeFS.ReadFile(path) + if err != nil { + return fmt.Errorf("locale: failed to read %s: %w", name, err) + } + + var b Bundle + if err := json.Unmarshal(data, &b); err != nil { + return fmt.Errorf("locale: failed to parse %s: %w", name, err) + } + + lang := Lang(b.Locale) + + register(lang, b) + } + + return nil +} diff --git a/internal/locale/locale.go b/internal/locale/locale.go new file mode 100644 index 0000000..113799b --- /dev/null +++ b/internal/locale/locale.go @@ -0,0 +1,67 @@ +package locale + +import ( + "strings" +) + +// Language code type +type Lang string + +const ( + EN_CA Lang = "en-CA" + FR_CA Lang = "fr-CA" + IU_CA Lang = "iu-CA" // Inuktitut (syllabics + roman in parentheses) +) + +// Bundle mirrors the JSON structure +type Bundle struct { + UI struct { + Loading string `json:"loading"` + FetchingWeather string `json:"fetching_weather"` + CurrentCond string `json:"current_conditions"` + FeelsLike string `json:"feels_like"` + Humidity string `json:"humidity"` + Wind string `json:"wind"` + Pressure string `json:"pressure"` + Visibility string `json:"visibility"` + Sunrise string `json:"sunrise"` + Sunset string `json:"sunset"` + Alerts string `json:"alerts"` + NoAlerts string `json:"no_alerts"` + Updated string `json:"updated"` + Location string `json:"location"` + Language string `json:"language"` + SelectLang string `json:"select_language"` + SelectLocation string `json:"select_location"` + ManualEntry string `json:"manual_entry"` + } `json:"ui"` + + Conditions map[string]string `json:"conditions"` + Alerts map[string]string `json:"alerts"` + + Locale string `json:"locale"` + Language string `json:"language_name"` + Script string `json:"script"` +} + +// Runtime loaded language bundles (populated by loader.go) +var bundles = map[Lang]Bundle{} + +// Register language bundle +func register(lang Lang, b Bundle) { + bundles[lang] = b +} + +// Get returns a language bundle, defaulting to EN_CA +func Get(lang Lang) Bundle { + if b, ok := bundles[lang]; ok { + return b + } + return bundles[EN_CA] +} + +// Normalize condition keys internally +func Normalize(cond string) string { + c := strings.ToLower(cond) + return strings.ReplaceAll(c, " ", "_") +} diff --git a/internal/output/colour.go b/internal/output/colour.go new file mode 100644 index 0000000..47374f5 --- /dev/null +++ b/internal/output/colour.go @@ -0,0 +1,144 @@ +package output + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// --------------------------------------------------------------------- +// RGB STRUCTS & HELPERS +// --------------------------------------------------------------------- + +type RGB struct { + R int + G int + B int +} + +func (c RGB) ANSI() string { + return fmt.Sprintf("\033[38;2;%d;%d;%dm", c.R, c.G, c.B) +} + +func RGBFromInts(r, g, b int) RGB { + return RGB{R: r, G: g, B: b} +} + +func RGBFromHex(hex string) RGB { + hex = strings.TrimPrefix(hex, "#") + if len(hex) != 6 { + return RGB{255, 255, 255} // fail-safe + } + r, _ := strconv.ParseInt(hex[0:2], 16, 0) + g, _ := strconv.ParseInt(hex[2:4], 16, 0) + b, _ := strconv.ParseInt(hex[4:6], 16, 0) + return RGB{int(r), int(g), int(b)} +} + +func Blend(a, b RGB, factor float64) RGB { + return RGB{ + R: int(float64(a.R)*(1-factor) + float64(b.R)*factor), + G: int(float64(a.G)*(1-factor) + float64(b.G)*factor), + B: int(float64(a.B)*(1-factor) + float64(b.B)*factor), + } +} + +// --------------------------------------------------------------------- +// TIME-OF-DAY COLOUR PHASES (CANADIAN ENGLISH) +// --------------------------------------------------------------------- + +// These were recreated from the original Python _get_time_phase logic. + +type Phase struct { + Name string + Primary RGB // top gradient + Secondary RGB // bottom gradient +} + +func getTimePhase() Phase { + h := time.Now().Hour() + + switch { + case h >= 5 && h < 8: + return Phase{ + Name: "dawn", + Primary: RGBFromHex("#FFB478"), // orange-peach morning sky + Secondary: RGBFromHex("#8C64A0"), // purple base + } + case h >= 8 && h < 17: + return Phase{ + Name: "day", + Primary: RGBFromHex("#4696FF"), // bright sky blue + Secondary: RGBFromHex("#78BEFF"), // lighter blue + } + case h >= 17 && h < 20: + return Phase{ + Name: "sunset", + Primary: RGBFromHex("#FF6E3C"), // deep orange + Secondary: RGBFromHex("#643C78"), // muted purple + } + default: + return Phase{ + Name: "night", + Primary: RGBFromHex("#323C64"), // deep navy + Secondary: RGBFromHex("#141E3C"), // darker base + } + } +} + +// Public so other modules (logo, TUI, Telefact) can use it. +func GetTimePhaseColours() (RGB, RGB, string) { + phase := getTimePhase() + return phase.Primary, phase.Secondary, phase.Name +} + +// --------------------------------------------------------------------- +// WEATHER TONES — CANADIAN ENGLISH (GREY, COLOUR, METRE, etc.) +// --------------------------------------------------------------------- + +// This is used as a tint on top of the base time-of-day palette. + +var WeatherTones = map[string]RGB{ + "sunny": RGBFromHex("#FFD700"), // gold + "clear": RGBFromHex("#FFD700"), + "cloudy": RGBFromHex("#B4B4B4"), + "mostly_cloudy": RGBFromHex("#B4B4B4"), + "rain": RGBFromHex("#2870C8"), + "drizzle": RGBFromHex("#2870C8"), + "snow": RGBFromHex("#E6E6F0"), + "fog": RGBFromHex("#82828C"), + "mist": RGBFromHex("#82828C"), + "thunderstorm": RGBFromHex("#C878FF"), + "smoke": RGBFromHex("#8C7864"), +} + +// Normalises Environment Canada text into our tone keys. +func NormaliseCondition(cond string) string { + c := strings.ToLower(cond) + + switch { + case strings.Contains(c, "sun"): + return "sunny" + case strings.Contains(c, "clear"): + return "clear" + case strings.Contains(c, "snow"): + return "snow" + case strings.Contains(c, "storm"): + return "thunderstorm" + case strings.Contains(c, "rain"): + return "rain" + case strings.Contains(c, "drizzle"): + return "drizzle" + case strings.Contains(c, "fog"): + return "fog" + case strings.Contains(c, "mist"): + return "mist" + case strings.Contains(c, "cloud"): + return "cloudy" + case strings.Contains(c, "smoke"): + return "smoke" + } + + return "clear" +} diff --git a/internal/output/formatter.go b/internal/output/formatter.go index 850ac2b..b5b571f 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/leaktechnologies/skyfeed/internal/weather" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/weather" ) // Theme defines simple ANSI colour codes for terminal output. diff --git a/internal/output/logo.go b/internal/output/logo.go new file mode 100644 index 0000000..b42230e --- /dev/null +++ b/internal/output/logo.go @@ -0,0 +1,179 @@ +package output + +import ( + "fmt" + "strings" + "time" +) + +// --------------------------------------------------------------------- +// SKYFEED ASCII LOGO +// --------------------------------------------------------------------- + +var SkyfeedLogoLines = []string{ + " ______ ___ __", + " / __/ /____ __/ _/__ ___ ___/ /", + " _\\ \\/ '_/ // / _/ -_) -_) _ / ", + "/___/_/\\_\\\\_, /_/ \\__/\\__/\\_,_/ ", + " /___/ ", +} + +// --------------------------------------------------------------------- +// ANSI HELPERS +// --------------------------------------------------------------------- + +func ansi(r, g, b int) string { + return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b) +} + +func reset() string { + return "\033[0m" +} + +// --------------------------------------------------------------------- +// TIME-OF-DAY PALETTES (MATCHING PYTHON LOGIC) +// --------------------------------------------------------------------- +// +// Python version used a function _get_time_phase() which returned +// two hex colours: primary (top) and secondary (bottom). +// We reproduce that behaviour exactly here. +// + +type PhaseColors struct { + Primary [3]int // top colour + Secondary [3]int // bottom colour +} + +func getTimePhaseColors() PhaseColors { + h := time.Now().Hour() + + switch { + case h >= 5 && h < 8: + // Dawn + return PhaseColors{ + Primary: [3]int{255, 180, 120}, + Secondary: [3]int{140, 100, 160}, + } + case h >= 8 && h < 17: + // Noon + return PhaseColors{ + Primary: [3]int{70, 150, 255}, + Secondary: [3]int{120, 190, 255}, + } + case h >= 17 && h < 20: + // Sunset + return PhaseColors{ + Primary: [3]int{255, 110, 60}, + Secondary: [3]int{100, 60, 120}, + } + default: + // Night + return PhaseColors{ + Primary: [3]int{50, 60, 100}, + Secondary: [3]int{20, 30, 60}, + } + } +} + +// --------------------------------------------------------------------- +// WEATHER TONE MAPPING (ANSI-256 FOREGROUND) +// +// These match the Python "tone_color_map" used for verbose mode, +// but here they are used as coloration tint. +// --------------------------------------------------------------------- + +var weatherToneMap = map[string][3]int{ + "sunny": {255, 215, 0}, // yellow(ish) + "clear": {255, 215, 0}, + "cloudy": {180, 180, 180}, + "mostly_cloudy": {180, 180, 180}, + "rain": {40, 110, 200}, + "drizzle": {40, 110, 200}, + "snow": {230, 230, 240}, + "fog": {130, 130, 140}, + "mist": {130, 130, 140}, + "thunderstorm": {200, 120, 255}, + "smoke": {140, 120, 100}, +} + +// Normalize a condition to a known tone key. +func normalizeCondition(cond string) string { + c := strings.ToLower(cond) + + switch { + case strings.Contains(c, "sun"): + return "sunny" + case strings.Contains(c, "clear"): + return "clear" + case strings.Contains(c, "snow"): + return "snow" + case strings.Contains(c, "storm"): + return "thunderstorm" + case strings.Contains(c, "rain"): + return "rain" + case strings.Contains(c, "drizzle"): + return "drizzle" + case strings.Contains(c, "fog"): + return "fog" + case strings.Contains(c, "mist"): + return "fog" + case strings.Contains(c, "cloud"): + return "cloudy" + case strings.Contains(c, "smoke"): + return "smoke" + } + + return "clear" +} + +// --------------------------------------------------------------------- +// BLENDING FUNCTIONS +// --------------------------------------------------------------------- + +func blend(a, b [3]int, factor float64) [3]int { + return [3]int{ + int(float64(a[0])*(1-factor) + float64(b[0])*factor), + int(float64(a[1])*(1-factor) + float64(b[1])*factor), + int(float64(a[2])*(1-factor) + float64(b[2])*factor), + } +} + +// --------------------------------------------------------------------- +// RENDER LOGO: RETURNS A STRING +// --------------------------------------------------------------------- + +func RenderLogoString(weatherCond string) string { + phase := getTimePhaseColors() + toneKey := normalizeCondition(weatherCond) + tone := weatherToneMap[toneKey] + + var b strings.Builder + total := len(SkyfeedLogoLines) + + for i, line := range SkyfeedLogoLines { + // Vertical gradient: bottom → top + factor := float64(i) / float64(total-1) + + // Blend primary→secondary vertically + lineCol := blend(phase.Secondary, phase.Primary, factor) + + // Weather tint applied lightly (0.0 → 0.25) + tintFactor := 0.25 + tinted := blend(lineCol, tone, tintFactor) + + b.WriteString(ansi(tinted[0], tinted[1], tinted[2])) + b.WriteString(line) + b.WriteString(reset()) + b.WriteString("\n") + } + + return b.String() +} + +// --------------------------------------------------------------------- +// PRINT LOGO — For CLI / debug-logo command +// --------------------------------------------------------------------- + +func PrintLogo(weatherCond string) { + fmt.Print(RenderLogoString(weatherCond)) +} diff --git a/internal/weather/cache.go b/internal/weather/cache.go index 062e590..6f0eaba 100644 --- a/internal/weather/cache.go +++ b/internal/weather/cache.go @@ -7,7 +7,7 @@ import ( "path/filepath" "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) // Cache file path diff --git a/internal/weather/fetch.go b/internal/weather/fetch.go index 1d3eb28..b65b680 100644 --- a/internal/weather/fetch.go +++ b/internal/weather/fetch.go @@ -12,7 +12,7 @@ import ( "strings" // ✅ Required for province normalization "time" - "github.com/leaktechnologies/skyfeed/internal/config" + "git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config" ) // WeatherData holds simplified normalized current weather data. diff --git a/skyfeed b/skyfeed new file mode 100755 index 0000000..94c81b0 Binary files /dev/null and b/skyfeed differ