14 Commits

Author SHA1 Message Date
d648d5a3f1 chore(changelog): update changelog for v0.4.0
All checks were successful
Upload Assets / upload-assets (amd64, linux) (release) Successful in 28s
Upload Assets / upload-assets (arm64, linux) (release) Successful in 27s
2025-05-28 16:17:26 +00:00
bb51982f6e chore(version): bump to 0.4.0
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / changelog-only (push) Has been skipped
CI / build (push) Successful in 6s
Auto Changelog & Release / release (push) Successful in 13s
2025-05-28 18:17:07 +02:00
3416610486 chore(changelog): update unreleased changelog 2025-05-28 16:15:06 +00:00
54d71ba3f0 style(i18n): add missing newline at EOF in JSON files
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 2s
CI / build (push) Successful in 7s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Ensures JSON files adhere to standard formatting by adding a newline
  at the end of the file.
2025-05-28 18:14:51 +02:00
c02da70902 feat(vscode): enable Deno support and configure JSON formatting
- Enables Deno integration in VS Code settings
- Sets the default JSON formatter to Deno's extension
- Activates format-on-save for improved workflow
2025-05-28 18:14:51 +02:00
9ad407e531 feat(ci): add CI workflow with format, lint, and test steps
- Introduces a CI workflow triggered on push, pull requests, and manual dispatch
- Includes steps for code formatting, linting, and testing using Deno
- Fails the workflow if any of the steps do not succeed
2025-05-28 18:14:51 +02:00
07730e5761 feat(tasks): add formatting, linting, and CI tasks
- Introduces new tasks for code formatting and linting
- Adds a CI task combining formatting, linting, and testing
2025-05-28 18:14:51 +02:00
1f79c1a15a chore(changelog): update unreleased changelog 2025-05-28 16:10:19 +00:00
440130f782 feat(tasks): include localization files in build commands
All checks were successful
Auto Changelog & Release / detect-version-change (push) Successful in 3s
Auto Changelog & Release / release (push) Has been skipped
Auto Changelog & Release / changelog-only (push) Successful in 6s
- Add German and English localization files to build outputs
- Ensure compiled binaries include required resources for i18n support
2025-05-28 18:10:04 +02:00
2a13ee2539 refactor(cli): integrate i18n support across commands
- Centralize CLI text strings using the i18n module for localization
- Refactor `createCommand` and `createCli` to improve modularity
- Update logging and error messages to use translated strings
2025-05-28 18:09:52 +02:00
8efbee1ba9 test(i18n): add unit tests for localization functions
- Add tests for `loadLocale` to verify translations load correctly
- Add tests for `t` to ensure fallback behavior for missing keys
- Add tests for `getCurrentLanguage` to validate language detection logic
2025-05-28 18:09:24 +02:00
bd5ea80aff feat(i18n): add German and English translations for CLI tool
- Introduce localization files for German and English languages
- Provide translations for CLI tool options and error messages
2025-05-28 18:09:16 +02:00
c9b4c8bd71 feat(i18n): add i18n module for localization support
- Introduce functions to initialize and load locale files dynamically
- Add support for translation keys with placeholder replacements
- Default to English if locale files are missing or not found
- Determine system language using environment variables
2025-05-28 18:09:04 +02:00
dfa92d8069 chore(vscode): update folder listener with i18n directory
- Adds the /src/i18n directory to the folder listener configuration
- Ensures changes in the i18n folder are tracked as part of the setup
2025-05-28 18:08:37 +02:00
16 changed files with 311 additions and 66 deletions

46
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,46 @@
name: CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Format
id: format
continue-on-error: true
run: deno task fmt
- name: Lint
id: lint
continue-on-error: true
run: deno task lint
- name: Test
id: test
continue-on-error: true
run: deno task test
- name: Fail if any step failed
if: |
steps.format.outcome != 'success' ||
steps.lint.outcome != 'success' ||
steps.test.outcome != 'success'
run: |
echo "::error::One or more steps failed"
exit 1

10
.vscode/settings.json vendored
View File

@@ -10,6 +10,12 @@
"peacock.color": "#c75c5c",
"exportall.config.folderListener": [
"/src/utils",
"/src/types"
]
"/src/types",
"/src/i18n"
],
"deno.enable": true,
"[json]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"editor.formatOnSave": true
}

View File

@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
## [0.4.0](https://git.0xmax42.io/maxp/systemd-timer/compare/v0.3.1..v0.4.0) - 2025-05-28
### 🚀 Features
- *(vscode)* Enable Deno support and configure JSON formatting - ([c02da70](https://git.0xmax42.io/maxp/systemd-timer/commit/c02da709028e1fbb175d5091fbd9d3ed2940cdcd))
- *(ci)* Add CI workflow with format, lint, and test steps - ([9ad407e](https://git.0xmax42.io/maxp/systemd-timer/commit/9ad407e531270445d9657402fa3e826a7dabd880))
- *(tasks)* Add formatting, linting, and CI tasks - ([07730e5](https://git.0xmax42.io/maxp/systemd-timer/commit/07730e576180be3f6a16b0fda6c6554a86844eee))
- *(tasks)* Include localization files in build commands - ([440130f](https://git.0xmax42.io/maxp/systemd-timer/commit/440130f782b1fc51053164410ead29397b867892))
- *(i18n)* Add German and English translations for CLI tool - ([bd5ea80](https://git.0xmax42.io/maxp/systemd-timer/commit/bd5ea80aff5092118920ea897af6c3f5f9fb2a3b))
- *(i18n)* Add i18n module for localization support - ([c9b4c8b](https://git.0xmax42.io/maxp/systemd-timer/commit/c9b4c8bd71029976fe900b40a2297b52200a216b))
### 🚜 Refactor
- *(cli)* Integrate i18n support across commands - ([2a13ee2](https://git.0xmax42.io/maxp/systemd-timer/commit/2a13ee2539d96d161a9ee398629fa79822d856f2))
### 🎨 Styling
- *(i18n)* Add missing newline at EOF in JSON files - ([54d71ba](https://git.0xmax42.io/maxp/systemd-timer/commit/54d71ba3f00ced25313036d9f10f6fb01feba52a))
### 🧪 Testing
- *(i18n)* Add unit tests for localization functions - ([8efbee1](https://git.0xmax42.io/maxp/systemd-timer/commit/8efbee1ba9b4fc564f5a32fcbc101ff256c5555b))
### ⚙️ Miscellaneous Tasks
- *(vscode)* Update folder listener with i18n directory - ([dfa92d8](https://git.0xmax42.io/maxp/systemd-timer/commit/dfa92d80694b5b104c26e131d1ee7c5cf69ad94c))
## [0.3.1](https://git.0xmax42.io/maxp/systemd-timer/compare/v0.2.3..v0.3.1) - 2025-05-28
### 🚀 Features

View File

@@ -1 +1 @@
0.3.1
0.4.0

View File

@@ -2,8 +2,11 @@
"tasks": {
"start": "deno run -A src/mod.ts",
"test": "deno test -A --coverage **/__tests__/*.test.ts",
"build:amd64": "deno compile --target x86_64-unknown-linux-gnu --include=VERSION --allow-env --allow-write --output dist/systemd-timer-linux-amd64 src/mod.ts",
"build:arm64": "deno compile --target aarch64-unknown-linux-gnu --include=VERSION --allow-env --allow-write --output dist/systemd-timer-linux-arm64 src/mod.ts"
"fmt": "deno fmt --check",
"lint": "deno lint",
"ci": "deno task fmt && deno task lint && deno task test", // For local CI checks
"build:amd64": "deno compile --target x86_64-unknown-linux-gnu --include VERSION --include src/i18n/de.json --include src/i18n/en.json --allow-env --allow-write --output dist/systemd-timer-linux-amd64 src/mod.ts",
"build:arm64": "deno compile --target aarch64-unknown-linux-gnu --include VERSION --include src/i18n/de.json --include src/i18n/en.json --allow-env --allow-write --output dist/systemd-timer-linux-arm64 src/mod.ts"
},
"compilerOptions": {},
"fmt": {

View File

@@ -1,51 +1,54 @@
import { Command } from '@cliffy/command';
import { generateUnitFiles } from '../templates/unit-generator.ts';
import { TimerOptions } from '../types/options.ts';
import { t } from '../i18n/mod.ts';
export const createCommand = new Command()
.description('Erzeugt eine systemd .service und .timer Unit')
.option(
'--name <name:string>',
'Name der Unit-Dateien (optional, wird sonst aus dem Exec generiert)',
)
.option(
'--exec <cmd:string>',
'Kommando, das durch systemd ausgeführt werden soll',
{ required: true },
)
.option('--calendar <time:string>', 'OnCalendar-Ausdruck für den Timer', {
required: true,
})
.option('--description <desc:string>', 'Beschreibung des Timers')
.option('--user', 'Erstellt die Unit als User-Timer')
.option(
'--run-as <user:string>',
'Führe den systemweiten Timer als bestimmter Benutzer aus (setzt User= in der Service-Unit)',
)
.option(
'--home <path:string>',
'HOME-Variable für den Service setzen',
)
.option(
'--cwd <path:string>',
'Arbeitsverzeichnis (WorkingDirectory) für den Service-Prozess',
)
.option('--output <dir:string>', 'Zielverzeichnis der Unit-Dateien')
.option(
'--after <target:string>',
'Optionales After= für die Service-Unit',
{ collect: true },
)
.option(
'--environment <env:string>',
'Environment-Variablen im Format KEY=VALUE',
{ collect: true },
)
.option(
'--logfile <file:string>',
'Dateipfad für Log-Ausgabe (stdout/stderr)',
)
.option('--dry-run', 'Gibt die Unit-Dateien nur aus, ohne sie zu schreiben')
.action(async (options: TimerOptions) => {
await generateUnitFiles(options);
});
export function createCommand() {
return new Command()
.description(t('cli_create_description'))
.option(
'--name <name:string>',
t('option_name'),
)
.option(
'--exec <cmd:string>',
t('option_exec'),
{ required: true },
)
.option('--calendar <time:string>', t('option_calendar'), {
required: true,
})
.option('--description <desc:string>', t('option_description'))
.option('--user', t('option_user'))
.option(
'--run-as <user:string>',
t('option_run_as'),
)
.option(
'--home <path:string>',
t('option_home'),
)
.option(
'--cwd <path:string>',
t('option_cwd'),
)
.option('--output <dir:string>', t('option_output'))
.option(
'--after <target:string>',
t('option_after'),
{ collect: true },
)
.option(
'--environment <env:string>',
t('option_environment'),
{ collect: true },
)
.option(
'--logfile <file:string>',
t('option_logfile'),
)
.option('--dry-run', t('option_dry_run'))
.action(async (options: TimerOptions) => {
await generateUnitFiles(options);
});
}

View File

@@ -1,10 +1,12 @@
import { Command } from '@cliffy/command';
import { createCommand } from './create.ts';
import { getVersion } from '../utils/mod.ts';
import { t } from '../i18n/mod.ts';
import { createCommand } from './mod.ts';
await new Command()
.name('systemd-timer')
.version(await getVersion())
.description('CLI Tool zum Erzeugen von systemd .timer und .service Units')
.command('create', createCommand)
.parse(Deno.args);
export async function createCli() {
return new Command()
.name('systemd-timer')
.version(await getVersion())
.description(t('cli_description'))
.command('create', createCommand());
}

2
src/cli/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export { createCommand } from './create.ts';
export { createCli } from './main.ts';

View File

@@ -0,0 +1,32 @@
import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { getCurrentLanguage, loadLocale, t } from '../mod.ts';
Deno.test('loadLocale lade Deutsche Übersetzung', async () => {
await loadLocale('de');
assertEquals(
t('error_write_units'),
'Fehler beim Schreiben der Units:',
);
assertEquals(
t('unit_written_service', { path: '/tmp/service' }),
'Service Unit geschrieben in: /tmp/service',
);
});
Deno.test('t gibt den Schlüssel zurück, wenn dieser nicht gefunden wird', () => {
const result = t('non_existent_key');
assertEquals(result, 'non_existent_key');
});
Deno.test('getCurrentLanguage gibt die Fallback Sprache zurück', () => {
Deno.env.delete('SYSTEMD_TIMER_LANG');
Deno.env.delete('LC_ALL');
Deno.env.delete('LC_MESSAGES');
Deno.env.delete('LANG');
assertEquals(getCurrentLanguage(), 'en');
});
Deno.test('getCurrentLanguage nutz die `SYSTEMD_TIMER_LANG`, wenn gesetzt', () => {
Deno.env.set('SYSTEMD_TIMER_LANG', 'de_DE.UTF-8');
assertEquals(getCurrentLanguage(), 'de');
});

22
src/i18n/de.json Normal file
View File

@@ -0,0 +1,22 @@
{
"cli_description": "CLI-Tool zum Erzeugen von systemd .timer und .service Units",
"cli_create_description": "Erzeugt eine systemd .service und .timer Unit",
"option_name": "Name der Unit-Dateien (optional, wird sonst aus dem Exec generiert)",
"option_exec": "Kommando, das durch systemd ausgeführt werden soll",
"option_calendar": "OnCalendar-Ausdruck für den Timer",
"option_description": "Beschreibung des Timers",
"option_user": "Erstellt die Unit als User-Timer",
"option_run_as": "Führe den systemweiten Timer als bestimmter Benutzer aus (setzt User= in der Service-Unit)",
"option_home": "HOME-Variable für den Service setzen",
"option_cwd": "Arbeitsverzeichnis (WorkingDirectory) für den Service-Prozess",
"option_output": "Zielverzeichnis der Unit-Dateien",
"option_after": "Optionales After= für die Service-Unit",
"option_environment": "Environment-Variablen im Format KEY=VALUE",
"option_logfile": "Dateipfad für Log-Ausgabe (stdout/stderr)",
"option_dry_run": "Gibt die Unit-Dateien nur aus, ohne sie zu schreiben",
"unit_written_service": "Service Unit geschrieben in: {path}",
"unit_written_timer": "Timer Unit geschrieben in: {path}",
"hint_header": "\nℹ️ Hinweis:",
"error_write_units": "Fehler beim Schreiben der Units:",
"rollback_failed": "Rollback fehlgeschlagen:"
}

22
src/i18n/en.json Normal file
View File

@@ -0,0 +1,22 @@
{
"cli_description": "CLI tool for generating systemd .timer and .service units",
"cli_create_description": "Generates a systemd .service and .timer unit",
"option_name": "Name of the unit files (optional, otherwise derived from the exec command)",
"option_exec": "Command to be executed by systemd",
"option_calendar": "OnCalendar expression for the timer",
"option_description": "Description of the timer",
"option_user": "Creates the unit as a user timer",
"option_run_as": "Runs the system-wide timer as a specific user (sets User= in the service unit)",
"option_home": "Sets the HOME variable for the service",
"option_cwd": "Working directory (WorkingDirectory) for the service process",
"option_output": "Target directory for the unit files",
"option_after": "Optional After= directive for the service unit",
"option_environment": "Environment variables in the format KEY=VALUE",
"option_logfile": "File path for log output (stdout/stderr)",
"option_dry_run": "Only outputs the unit files without writing them",
"unit_written_service": "Service unit written to: {path}",
"unit_written_timer": "Timer unit written to: {path}",
"hint_header": "\nℹ️ Note:",
"error_write_units": "Error while writing unit files:",
"rollback_failed": "Rollback failed:"
}

74
src/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,74 @@
/**
* Initializes the i18n module by loading
* the appropriate locale file based on the system language.
*/
export async function initI18n(): Promise<void> {
await loadLocale(getCurrentLanguage());
}
/**
* Contains the loaded translations for the current language.
* The keys represent i18n identifiers, the values are the localized strings.
*/
let translations: Record<string, string> = {};
/**
* Loads the translation file for the specified locale.
*
* Expects a JSON file in the same directory named like `de.json` or `en.json`.
* Falls back to English ('en') if the specified file does not exist.
*
* @param locale - The language code (e.g., 'de', 'en') to load
* @returns Promise that resolves once the translations have been loaded
*/
export async function loadLocale(locale: string): Promise<void> {
try {
const localeUrl = new URL(`./${locale}.json`, import.meta.url);
const file = await Deno.readTextFile(localeUrl);
translations = JSON.parse(file);
} catch (err) {
if (err instanceof Deno.errors.NotFound) {
console.warn(
`Locale '${locale}' not found – falling back to 'en'.`,
);
if (locale !== 'en') {
await loadLocale('en');
}
} else {
console.error('Error loading translation file:', err);
}
}
}
/**
* Translates a given key using the loaded translation data.
* Replaces placeholders in the format `{variable}` with the provided values.
*
* @param key - The i18n key (e.g., 'timer_created')
* @param vars - Optional replacements for placeholders in the string
* @returns The translated string, or the key itself if no translation is found
*/
export function t(key: string, vars: Record<string, string> = {}): string {
let text = translations[key] ?? key;
for (const [k, v] of Object.entries(vars)) {
text = text.replace(`{${k}}`, v);
}
return text;
}
/**
* Determines the current language from the system environment.
*
* Priority: SYSTEMD_TIMER_LANG > LC_ALL > LC_MESSAGES > LANG.
*
* @returns The language code (e.g., 'de', 'en'), defaults to 'en'
*/
export function getCurrentLanguage(): string {
return (
Deno.env.get('SYSTEMD_TIMER_LANG') ??
Deno.env.get('LC_ALL') ??
Deno.env.get('LC_MESSAGES') ??
Deno.env.get('LANG') ??
'en'
).split('.')[0].split('_')[0].toLowerCase();
}

1
src/i18n/mod.ts Normal file
View File

@@ -0,0 +1 @@
export { getCurrentLanguage, initI18n, loadLocale, t } from './i18n.ts';

View File

@@ -1,5 +1,8 @@
import './cli/main.ts';
import { createCli } from './cli/mod.ts';
import { initI18n } from './i18n/mod.ts';
// ────────────────────────────────────────────────
// Entry Point for CLI
// Delegates to src/cli/main.ts, which registers all CLI commands
await initI18n();
await (await createCli()).parse(Deno.args);

View File

@@ -1,3 +1,4 @@
import { t } from '../i18n/mod.ts';
import { TimerOptions } from '../types/mod.ts';
import { deriveNameFromExec, writeUnitFiles } from '../utils/mod.ts';
@@ -21,13 +22,13 @@ export async function generateUnitFiles(options: TimerOptions): Promise<void> {
if (result) {
const { servicePath, timerPath } = result;
console.log(`Service Unit geschrieben in: ${servicePath}`);
console.log(`Timer Unit geschrieben in: ${timerPath}`);
console.log(t('unit_written_service', { path: servicePath }));
console.log(t('unit_written_timer', { path: timerPath }));
} else {
return;
}
console.log(`\nℹ️ Hinweis:`);
console.log(t('hint_header'));
if (options.user) {
console.log(` systemctl --user daemon-reload`);

View File

@@ -1,6 +1,7 @@
import { ensureDir, exists } from 'https://deno.land/std@0.224.0/fs/mod.ts';
import { join } from 'https://deno.land/std@0.224.0/path/mod.ts';
import { TimerOptions } from '../types/mod.ts';
import { t } from '../i18n/mod.ts';
export async function writeUnitFiles(
name: string,
@@ -28,9 +29,9 @@ export async function writeUnitFiles(
await Deno.remove(timerPath);
}
} catch (rollbackError) {
console.error('Rollback fehlgeschlagen:', rollbackError);
console.error(t('rollback_failed'), rollbackError);
}
console.error('Fehler beim Schreiben der Units:', error);
console.error(t('error_write_units'), error);
return undefined;
}