Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
d648d5a3f1 | |||
bb51982f6e
|
|||
3416610486 | |||
54d71ba3f0
|
|||
c02da70902
|
|||
9ad407e531
|
|||
07730e5761
|
|||
1f79c1a15a | |||
440130f782
|
|||
2a13ee2539
|
|||
8efbee1ba9
|
|||
bd5ea80aff
|
|||
c9b4c8bd71
|
|||
dfa92d8069
|
46
.gitea/workflows/ci.yml
Normal file
46
.gitea/workflows/ci.yml
Normal 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
10
.vscode/settings.json
vendored
@@ -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
|
||||
}
|
27
CHANGELOG.md
27
CHANGELOG.md
@@ -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
|
||||
|
@@ -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": {
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
@@ -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
2
src/cli/mod.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { createCommand } from './create.ts';
|
||||
export { createCli } from './main.ts';
|
32
src/i18n/__tests__/i18n.test.ts
Normal file
32
src/i18n/__tests__/i18n.test.ts
Normal 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
22
src/i18n/de.json
Normal 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
22
src/i18n/en.json
Normal 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
74
src/i18n/i18n.ts
Normal 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
1
src/i18n/mod.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { getCurrentLanguage, initI18n, loadLocale, t } from './i18n.ts';
|
@@ -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);
|
||||
|
@@ -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`);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user