v0.1.0-dev2: Cleanup imports, relocate i18n, fix locale loader, ensure build stability

This commit is contained in:
Stu Leak 2025-11-13 20:35:09 -05:00
parent 16b8a8feb6
commit 7f00667213
26 changed files with 1705 additions and 87 deletions

View File

@ -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)
},
}

View File

@ -0,0 +1,9 @@
{
"language": "en-CA",
"location": {
"mode": "auto",
"manual_city": "",
"manual_lat": 0,
"manual_lon": 0
}
}

42
docs/CHANGELOG.md Normal file
View File

@ -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

285
docs/LANGUAGE_SUPPORT.md Normal file
View File

@ -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

26
docs/TAGGING.md Normal file
View File

@ -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

388
docs/TAXONOMY.md Normal file
View File

@ -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

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/leaktechnologies/skyfeed
module git.leaktechnologies.dev/Leak_Technologies/Skyfeed
go 1.25.4

View File

@ -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"
)

View File

@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/leaktechnologies/skyfeed/internal/config"
"git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config"
)
const (

View File

@ -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.

View File

@ -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).

View File

@ -9,7 +9,7 @@ import (
"path/filepath"
"time"
"github.com/leaktechnologies/skyfeed/internal/config"
"git.leaktechnologies.dev/Leak_Technologies/Skyfeed/internal/config"
)
const (

5
internal/locale/en_CA.go Normal file
View File

@ -0,0 +1,5 @@
package locale
func init() {
// Loaded automatically from en_CA.json via LoadAll()
}

5
internal/locale/fr_CA.go Normal file
View File

@ -0,0 +1,5 @@
package locale
func init() {
// Loaded automatically from fr_CA.json
}

View File

@ -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"
}
}

View File

@ -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 dorage violent",
"severe_thunderstorm_watch": "Veille dorage 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 lair",
"air_quality_advisory": "Avis sur la qualité de lair",
"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 douragan",
"hurricane_watch": "Veille douragan",
"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"
}
}

View File

@ -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)"
}
}

5
internal/locale/iu_CA.go Normal file
View File

@ -0,0 +1,5 @@
package locale
func init() {
// Loaded automatically from iu_CA.json
}

41
internal/locale/loader.go Normal file
View File

@ -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
}

67
internal/locale/locale.go Normal file
View File

@ -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, " ", "_")
}

144
internal/output/colour.go Normal file
View File

@ -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"
}

View File

@ -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.

179
internal/output/logo.go Normal file
View File

@ -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))
}

View File

@ -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

View File

@ -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.

BIN
skyfeed Executable file

Binary file not shown.