Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ Use the same literal configuration path for startup, logs, IPC, and process sele

## Run the regression suites

The six shell suites use isolated fixtures for lifecycle, installer, updater, theme, package, and AI usage behavior.
The seven shell suites use isolated fixtures for lifecycle, installer, updater, theme, package, AI usage, and localization behavior.

Run them from the repository root:

```bash
./tests/qs-arch-update-regression.sh
./tests/qs-barctl-regression.sh
./tests/qs-codex-usage-regression.sh
./tests/qs-i18n-regression.sh
./tests/qs-quattro-runtime-regression.sh
./tests/qs-shell-update-regression.sh
./tests/qs-theme-update-regression.sh
Expand Down Expand Up @@ -112,7 +113,7 @@ The shell updater compares the installed commit with the repository’s `main` b
Before updating `main`:

1. Group code and documentation into reviewable commits
2. Run all six regression suites
2. Run all seven regression suites
3. Run shell syntax, ShellCheck, QML, and whitespace checks
4. Test V1 and V2 through the installed controller
5. Confirm installer, updater, hooks, and controller come from the same commit
Expand Down
6 changes: 6 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ $HOME/.config/quickshell/bin/qs-barctl switch-wait v2

The shared host closes variant panels, destroys the old interface, loads the target, and waits for `ready=true`. It stores the new choice only after the target remains ready.

## Use localized date and weather text

Clock tooltips, calendar month/day labels, and weather-panel controls follow the process locale. Rise checks `LC_ALL`, then `LC_MESSAGES`, then `LANG`. Italian locales use the included Italian strings and `it_IT` date formatting; other locales currently fall back to English and `en_US`.

The shared helper lives at `versions/V1/i18n/Translation.qml`, so additional languages can extend one stable translation table without duplicating locale logic in V1 and V2.

## Choose workspace and bar styles

Workspace count and visual style are independent settings. Both variants support 10, 5, and active-only workspace modes.
Expand Down
60 changes: 60 additions & 0 deletions tests/qs-i18n-regression.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"

fail() {
printf 'i18n regression: %s\n' "$*" >&2
exit 1
}

require_contains() {
local file=$1 needle=$2
grep -Fq -- "$needle" "$file" || fail "$file missing: $needle"
}

require_absent() {
local file=$1 needle=$2
if grep -Fq -- "$needle" "$file"; then
fail "$file still contains: $needle"
fi
}

helper=versions/V1/i18n/Translation.qml
require_contains "$helper" 'Quickshell.env("LC_ALL")'
require_contains "$helper" 'Quickshell.env("LC_MESSAGES")'
require_contains "$helper" 'Quickshell.env("LANG")'
require_contains "$helper" 'readonly property string localeName: language === "it" ? "it_IT" : "en_US"'
require_contains "$helper" '"Weather": "Meteo"'
require_contains "$helper" '"Today": "Oggi"'

for clock in \
versions/V1/modules/ClockWidget.qml \
versions/V1/variants/V2/modules/ClockWidget.qml; do
require_contains "$clock" 'Translation { id: i18n }'
require_contains "$clock" 'toLocaleString(Qt.locale(i18n.localeName), "ddd, d MMMM yyyy")'
require_absent "$clock" 'readonly property var months: ["January"'
require_absent "$clock" 'readonly property var days: ["Sun"'
done

for calendar in \
versions/V1/panels/CalendarPopup.qml \
versions/V1/variants/V2/panels/CalendarPopup.qml; do
require_contains "$calendar" 'Translation { id: i18n }'
require_contains "$calendar" 'i18n.monthName('
require_contains "$calendar" 'i18n.weekdayShort(1)'
require_absent "$calendar" 'model: ["MO","TU","WE","TH","FR","SA","SU"]'
done

for weather in \
versions/V1/panels/WeatherPanel.qml \
versions/V1/variants/V2/panels/WeatherPanel.qml; do
require_contains "$weather" 'Translation { id: i18n }'
require_contains "$weather" 'return i18n.tr("Today")'
require_contains "$weather" 'return d.toLocaleString(Qt.locale(i18n.localeName), "ddd")'
require_contains "$weather" 'text: i18n.tr("Weather")'
require_contains "$weather" 'i18n.tr("Refreshing…")'
done

printf 'i18n regression checks passed\n'
54 changes: 54 additions & 0 deletions versions/V1/i18n/Translation.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import QtQuick
import Quickshell

QtObject {
id: translation

readonly property string rawLocale: Quickshell.env("LC_ALL") !== ""
? Quickshell.env("LC_ALL")
: Quickshell.env("LC_MESSAGES") !== ""
? Quickshell.env("LC_MESSAGES")
: Quickshell.env("LANG")
readonly property string language: normalizeLanguage(rawLocale)
readonly property string localeName: language === "it" ? "it_IT" : "en_US"

readonly property var italian: ({
"Weather": "Meteo",
"Today": "Oggi",
"Tomorrow": "Domani",
"Location": "Località",
"Feels like": "Percepita",
"Humidity": "Umidità",
"Wind": "Vento",
"3-DAY FORECAST": "PREVISIONI 3 GIORNI",
"rain": "pioggia",
"Refreshing…": "Aggiornamento…",
"Refresh": "Aggiorna",
"metric": "metrico",
"imperial": "imperiale"
})

function normalizeLanguage(value) {
var candidate = String(value || "").trim().toLowerCase().replace("-", "_")
return candidate.indexOf("it") === 0 ? "it" : "en"
}

function tr(source) {
var value = String(source || "")
if (language === "it" && italian[value] !== undefined)
return italian[value]
return value
}

function weekdayShort(dayOfWeek) {
// 2023-01-01 was a Sunday; offset from it to get a stable weekday name.
var date = new Date(2023, 0, 1 + dayOfWeek)
return date.toLocaleString(Qt.locale(localeName), "ddd").slice(0, 2).toUpperCase()
}

function monthName(year, month) {
return new Date(year, month, 1)
.toLocaleString(Qt.locale(localeName), "MMMM")
.toUpperCase()
}
}
10 changes: 5 additions & 5 deletions versions/V1/modules/ClockWidget.qml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../i18n"

Item {
id: rootMod
required property var root

Translation { id: i18n }

property date now: new Date()

function pad(n) { return n < 10 ? "0" + n : String(n) }
Expand All @@ -18,11 +21,8 @@ Item {
return pad(now.getHours()) + ":" + pad(now.getMinutes())
}

readonly property var months: ["January","February","March","April","May","June",
"July","August","September","October","November","December"]
readonly property var days: ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

readonly property string tooltipText: days[now.getDay()] + ", " + now.getDate() + " " + months[now.getMonth()] + " " + now.getFullYear()
readonly property string tooltipText:
now.toLocaleString(Qt.locale(i18n.localeName), "ddd, d MMMM yyyy")

implicitWidth: label.implicitWidth
implicitHeight: 28
Expand Down
17 changes: 15 additions & 2 deletions versions/V1/panels/CalendarPopup.qml
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import QtQuick
import "../modules"
import "../i18n"
import Quickshell
import Quickshell.Wayland

PanelWindow {
id: calPopup
required property var root

Translation { id: i18n }

function monthTitle() {
var now = new Date()
var month = now.getMonth() + root.calendarMonthOffset
return i18n.monthName(now.getFullYear(), month) + " " + root.calendarYear
}

screen: root.activePopupScreen

color: "transparent"
Expand Down Expand Up @@ -92,7 +101,7 @@ PanelWindow {
// month + year — click to jump back to today
UiText {
anchors.centerIn: parent
text: root.calendarMonthName + " " + root.calendarYear
text: calPopup.monthTitle()
color: monthMa.containsMouse && root.calendarMonthOffset !== 0 ? root.seal : root.ink
font.family: root.mono
font.pixelSize: 12
Expand Down Expand Up @@ -135,7 +144,11 @@ PanelWindow {
Row {
width: parent.width
Repeater {
model: ["MO","TU","WE","TH","FR","SA","SU"]
model: [
i18n.weekdayShort(1), i18n.weekdayShort(2), i18n.weekdayShort(3),
i18n.weekdayShort(4), i18n.weekdayShort(5), i18n.weekdayShort(6),
i18n.weekdayShort(0)
]
delegate: Item {
required property string modelData
required property int index
Expand Down
27 changes: 15 additions & 12 deletions versions/V1/panels/WeatherPanel.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QtQuick
import "../modules"
import "../i18n"
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
Expand All @@ -8,6 +9,8 @@ PanelWindow {
id: wxPanel
required property var root

Translation { id: i18n }

screen: root.activePopupScreen

color: "transparent"
Expand Down Expand Up @@ -58,11 +61,11 @@ PanelWindow {
return String.fromCodePoint(0xe33d)
}
function dayLabel(dateStr, index) {
if (index === 0) return "Today"
if (index === 1) return "Tomorrow"
if (index === 0) return i18n.tr("Today")
if (index === 1) return i18n.tr("Tomorrow")
var d = new Date(dateStr + "T00:00:00")
if (isNaN(d.getTime())) return dateStr
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][d.getDay()]
return d.toLocaleString(Qt.locale(i18n.localeName), "ddd")
}
function dayRange(day) {
var unit = root.weatherImperial ? "°F" : "°C"
Expand Down Expand Up @@ -156,7 +159,7 @@ PanelWindow {
height: 24
UiText {
anchors.left: parent.left; anchors.verticalCenter: parent.verticalCenter
text: "Weather"
text: i18n.tr("Weather")
color: root.ink; font.family: root.mono; font.pixelSize: 13
font.letterSpacing: 2; font.weight: Font.Medium
}
Expand Down Expand Up @@ -193,25 +196,25 @@ PanelWindow {
Row {
width: parent.width
visible: wxPanel.location !== ""
UiText { text: "Location"; color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: i18n.tr("Location"); color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: wxPanel.location; color: root.ink; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.6; elide: Text.ElideRight }
}
Row {
width: parent.width
visible: wxPanel.feels !== ""
UiText { text: "Feels like"; color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: i18n.tr("Feels like"); color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: wxPanel.tConv(wxPanel.feels) + "°" + (root.weatherImperial ? "F" : "C"); color: root.ink; font.family: root.mono; font.pixelSize: 11 }
}
Row {
width: parent.width
visible: wxPanel.humidity !== ""
UiText { text: "Humidity"; color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: i18n.tr("Humidity"); color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: wxPanel.humidity + "%"; color: root.ink; font.family: root.mono; font.pixelSize: 11 }
}
Row {
width: parent.width
visible: wxPanel.wind !== ""
UiText { text: "Wind"; color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: i18n.tr("Wind"); color: root.sumiHi; font.family: root.mono; font.pixelSize: 11; width: parent.width * 0.4 }
UiText { text: wxPanel.wConv(wxPanel.wind); color: root.ink; font.family: root.mono; font.pixelSize: 11 }
}
}
Expand All @@ -224,7 +227,7 @@ PanelWindow {
visible: wxPanel.forecastDays.length > 0

UiText {
text: "3-DAY FORECAST"
text: i18n.tr("3-DAY FORECAST")
color: root.sumiHi
font.family: root.mono
font.pixelSize: 10
Expand Down Expand Up @@ -271,7 +274,7 @@ PanelWindow {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 76
text: (day.rain !== undefined ? Math.round(day.rain) + "% rain" : "")
text: (day.rain !== undefined ? Math.round(day.rain) + "% " + i18n.tr("rain") : "")
color: root.sumiHi
font.family: root.mono
font.pixelSize: 10
Expand All @@ -296,7 +299,7 @@ PanelWindow {
Behavior on color { ColorAnimation { duration: 120 } }
UiText {
anchors.centerIn: parent
text: wxPanel.refreshing ? "Refreshing…" : "Refresh"
text: wxPanel.refreshing ? i18n.tr("Refreshing…") : i18n.tr("Refresh")
color: root.paper; font.family: root.mono; font.pixelSize: 11
}
MouseArea {
Expand All @@ -317,7 +320,7 @@ PanelWindow {
Behavior on border.color { ColorAnimation { duration: 120 } }
UiText {
anchors.centerIn: parent
text: root.weatherImperial ? "metric" : "imperial"
text: root.weatherImperial ? i18n.tr("metric") : i18n.tr("imperial")
color: unitMa.containsMouse ? root.seal : root.ink
font.family: root.mono; font.pixelSize: 11
Behavior on color { ColorAnimation { duration: 120 } }
Expand Down
10 changes: 5 additions & 5 deletions versions/V1/variants/V2/modules/ClockWidget.qml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../../../i18n"

Item {
id: rootMod
required property var root

Translation { id: i18n }

property date now: new Date()
readonly property color contentColor: root.widgetContentColor("G8", root.ink)

Expand All @@ -19,11 +22,8 @@ Item {
return pad(now.getHours()) + ":" + pad(now.getMinutes())
}

readonly property var months: ["January","February","March","April","May","June",
"July","August","September","October","November","December"]
readonly property var days: ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]

readonly property string tooltipText: days[now.getDay()] + ", " + now.getDate() + " " + months[now.getMonth()] + " " + now.getFullYear()
readonly property string tooltipText:
now.toLocaleString(Qt.locale(i18n.localeName), "ddd, d MMMM yyyy")

implicitWidth: label.implicitWidth
implicitHeight: 28
Expand Down
Loading