Add i18n support and translations for finnish language
This commit is contained in:
parent
b88e19e311
commit
4b8c8be226
9 changed files with 239 additions and 84 deletions
|
@ -2,27 +2,11 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import { xdgConfig } from 'xdg-basedir';
|
||||
import toml from '@iarna/toml';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Duration } from 'dayjs/plugin/duration.js';
|
||||
import { parseDuration, parseTimestamp } from './parse.js';
|
||||
import WtcConfig from './types/WtcConfig.js';
|
||||
import Language from './types/Language.js';
|
||||
|
||||
interface Config {
|
||||
defaults: {
|
||||
workDayDuration: Duration;
|
||||
lunchBreakDuration: Duration;
|
||||
startTime: Dayjs;
|
||||
stopTime: Dayjs;
|
||||
};
|
||||
askInput: {
|
||||
workDayLength: boolean;
|
||||
startTime: boolean;
|
||||
stopTime: boolean;
|
||||
logged: boolean;
|
||||
hadLunch: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface RawConfig extends Omit<Config, 'defaults'> {
|
||||
interface RawConfig extends Omit<WtcConfig, 'defaults'> {
|
||||
defaults: {
|
||||
workDayDuration: string;
|
||||
lunchBreakDuration: string;
|
||||
|
@ -32,6 +16,7 @@ interface RawConfig extends Omit<Config, 'defaults'> {
|
|||
}
|
||||
|
||||
const defaultConfig: RawConfig = {
|
||||
language: Language.en,
|
||||
defaults: {
|
||||
workDayDuration: '07:30',
|
||||
lunchBreakDuration: '00:30',
|
||||
|
@ -47,9 +32,9 @@ const defaultConfig: RawConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const getConfig = (): Config => {
|
||||
const getConfig = (): WtcConfig => {
|
||||
const configDir = xdgConfig || path.join(process.env.HOME ?? './', '.config');
|
||||
let configFilePath = path.join(configDir, 'wct', 'config.toml');
|
||||
let configFilePath = path.join(configDir, 'wtc', 'config.toml');
|
||||
|
||||
let configData: RawConfig;
|
||||
if (fs.existsSync(configFilePath)) {
|
||||
|
@ -59,6 +44,7 @@ const getConfig = (): Config => {
|
|||
}
|
||||
|
||||
return {
|
||||
language: configData.language ?? defaultConfig.language,
|
||||
defaults: {
|
||||
workDayDuration: parseDuration(
|
||||
configData.defaults.workDayDuration ?? defaultConfig.defaults.workDayDuration,
|
||||
|
|
|
@ -1,28 +1,44 @@
|
|||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { Duration } from 'dayjs/plugin/duration.js';
|
||||
import Language from './types/Language';
|
||||
import { MessageKey, message } from './i18n';
|
||||
|
||||
export const formatTimestamp = (timestamp: Dayjs): string => timestamp.format('YYYY-MM-DD HH:mm');
|
||||
|
||||
export const formatTime = (time: Dayjs): string => time.format('HH:mm');
|
||||
|
||||
export const formatDuration = (duration: Duration, short?: boolean): string => {
|
||||
if (duration.hours() === 0 && duration.minutes() === 0) {
|
||||
return 'none';
|
||||
}
|
||||
export const formatDuration =
|
||||
(language: Language) =>
|
||||
(duration: Duration, short?: boolean): string => {
|
||||
if (duration.hours() === 0 && duration.minutes() === 0) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
const formatString = short
|
||||
? 'HH:mm'
|
||||
: duration.hours() > 0 && duration.minutes() > 0
|
||||
? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]`
|
||||
: duration.hours() > 0
|
||||
? `H [hour${duration.hours() > 1 ? 's' : ''}]`
|
||||
: `m [minute${duration.minutes() > 1 ? 's' : ''}]`;
|
||||
let formatString;
|
||||
|
||||
return duration.format(formatString);
|
||||
};
|
||||
if (short) {
|
||||
formatString = 'HH:mm';
|
||||
} else if (language === Language.fi) {
|
||||
formatString =
|
||||
duration.hours() > 0 && duration.minutes() > 0
|
||||
? `H [tunti${duration.hours() > 1 ? 'a' : ''} ja] m [minuutti${duration.minutes() > 1 ? 'a' : ''}]`
|
||||
: duration.hours() > 0
|
||||
? `H [tunti${duration.hours() > 1 ? 'a' : ''}]`
|
||||
: `m [minutti${duration.minutes() > 1 ? 'a' : ''}]`;
|
||||
} else {
|
||||
formatString =
|
||||
duration.hours() > 0 && duration.minutes() > 0
|
||||
? `H [hour${duration.hours() > 1 ? 's' : ''} and] m [minute${duration.minutes() > 1 ? 's' : ''}]`
|
||||
: duration.hours() > 0
|
||||
? `H [hour${duration.hours() > 1 ? 's' : ''}]`
|
||||
: `m [minute${duration.minutes() > 1 ? 's' : ''}]`;
|
||||
}
|
||||
|
||||
export const getHoursRoundedStr = (duration: Duration) =>
|
||||
`(${getHoursRounded(duration)} as hours rounded to next even 15 minutes)`;
|
||||
return duration.format(formatString);
|
||||
};
|
||||
|
||||
export const getHoursRoundedStr = (language: Language) => (duration: Duration) =>
|
||||
`(${getHoursRounded(duration)} ${message(language)(MessageKey.hoursRounded)})`;
|
||||
|
||||
const getHoursRounded = (duration: Duration) => {
|
||||
// Round up to the next multiple of 15
|
||||
|
|
120
src/i18n.ts
Normal file
120
src/i18n.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
import Language from './types/Language';
|
||||
|
||||
export enum MessageKey {
|
||||
promptWorkDayDuration,
|
||||
promptStartTime,
|
||||
promptStopTime,
|
||||
parseTimeFailed,
|
||||
startTimeBeforeStopTimeError,
|
||||
promptLunchBreak,
|
||||
promptLogged,
|
||||
none,
|
||||
startedWorking,
|
||||
stoppedWorking,
|
||||
workedToday,
|
||||
loggedOver,
|
||||
workedOvertime,
|
||||
workLeft,
|
||||
hoursCalculated,
|
||||
klo,
|
||||
unloggedToday,
|
||||
hoursRounded,
|
||||
}
|
||||
|
||||
const messages: Record<MessageKey, Record<Language, string>> = {
|
||||
[MessageKey.promptWorkDayDuration]: {
|
||||
[Language.en]: 'How long is your work day today, excluding the lunch break? [{0}]: ',
|
||||
[Language.fi]: 'Kuinka pitkä työpäiväsi on tänään, poisluettuna lounastauko? [{0}]: ',
|
||||
},
|
||||
[MessageKey.promptStartTime]: {
|
||||
[Language.en]: 'What time did you start work today? [{0}]: ',
|
||||
[Language.fi]: 'Mihin aikaan aloitit työskentelyn tänään? [{0}]: ',
|
||||
},
|
||||
[MessageKey.promptStopTime]: {
|
||||
[Language.en]: "What time did you stop working? If you didn't stop yet, leave this empty: ",
|
||||
[Language.fi]: 'Mihin aikaan lopetit työskentelyn? Jos et lopettanut vielä, jätä tämä tyhjäksi: ',
|
||||
},
|
||||
[MessageKey.parseTimeFailed]: {
|
||||
[Language.en]: 'Failed to parse time "{0}", using default value "{1}"',
|
||||
[Language.fi]: 'Ajan "{0}" parsiminen epäonnistui, käytetään oletusasetusta "{1}"',
|
||||
},
|
||||
[MessageKey.startTimeBeforeStopTimeError]: {
|
||||
[Language.en]: 'Start time ({0}) needs to be before stop time ({1}). Exiting',
|
||||
[Language.fi]: 'Aloitusaika ({0}) pitää olla ennen lopetusaikaa ({1}). Ohjelma sammuu',
|
||||
},
|
||||
[MessageKey.promptLunchBreak]: {
|
||||
[Language.en]: 'Did you have a lunch break? [y/N]: ',
|
||||
[Language.fi]: 'Piditkö jo lounastauon? [k/E]: ',
|
||||
},
|
||||
[MessageKey.promptLogged]: {
|
||||
[Language.en]: 'How many hours did you log already? [00:00] ',
|
||||
[Language.fi]: 'Kuinka monta tuntia kirjasit jo? [00:00] ',
|
||||
},
|
||||
[MessageKey.none]: {
|
||||
[Language.en]: 'None',
|
||||
[Language.fi]: 'Ei yhtään',
|
||||
},
|
||||
[MessageKey.startedWorking]: {
|
||||
[Language.en]: 'Started working:',
|
||||
[Language.fi]: 'Aloitit työskentelyn:',
|
||||
},
|
||||
[MessageKey.stoppedWorking]: {
|
||||
[Language.en]: 'Stopped working',
|
||||
[Language.fi]: 'Lopetit työskentelyn',
|
||||
},
|
||||
[MessageKey.hoursCalculated]: {
|
||||
[Language.en]: 'Hours calculated',
|
||||
[Language.fi]: 'Tunnit laskettu',
|
||||
},
|
||||
[MessageKey.workedToday]: {
|
||||
[Language.en]: 'Worked today:',
|
||||
[Language.fi]: 'Tänään työskennelty:',
|
||||
},
|
||||
[MessageKey.workedOvertime]: {
|
||||
[Language.en]: 'You worked {0} overtime today',
|
||||
[Language.fi]: 'Olet tehnyt {0} ylitöitä tänään',
|
||||
},
|
||||
[MessageKey.loggedOver]: {
|
||||
[Language.en]: 'You have logged {0} more than you worked today!',
|
||||
[Language.fi]: 'Olet kirjannut {0} enemmän kuin olet työskennellyt!',
|
||||
},
|
||||
[MessageKey.workLeft]: {
|
||||
[Language.en]: 'You still have to work {0} more today',
|
||||
[Language.fi]: 'Sinun pitää työskennellä tänään vielä {0} lisää',
|
||||
},
|
||||
[MessageKey.klo]: {
|
||||
[Language.en]: 'at',
|
||||
[Language.fi]: 'klo',
|
||||
},
|
||||
[MessageKey.unloggedToday]: {
|
||||
[Language.en]: 'Unlogged today:',
|
||||
[Language.fi]: 'Kirjaamattomia tänään:',
|
||||
},
|
||||
[MessageKey.hoursRounded]: {
|
||||
[Language.en]: 'as hours rounded to next even 15 minutes',
|
||||
[Language.fi]: 'tunteina pyöristettynä seuraavaan 15 minuuttiin',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a function to fetch messages for a given language
|
||||
* @param language The language to get the messages for
|
||||
*/
|
||||
export const message =
|
||||
(language: Language) =>
|
||||
/**
|
||||
* Get a message for a fiven key
|
||||
* @param key The key of the message
|
||||
*/
|
||||
(key: keyof typeof messages, ...params: string[]) => {
|
||||
let result = messages[key][language];
|
||||
if (!result) {
|
||||
throw `Unknown language: ${language}`;
|
||||
}
|
||||
|
||||
// Replace parameters in the template
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
result = result.replace(new RegExp(`\\{${i}\\}`, 'g'), params[i]);
|
||||
}
|
||||
return result;
|
||||
};
|
52
src/input.ts
52
src/input.ts
|
@ -7,13 +7,17 @@ import { formatDuration, formatTime } from './format';
|
|||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { WtcPromptResult } from './types/WtcPromptResult';
|
||||
import duration from 'dayjs/plugin/duration.js';
|
||||
import WtcConfig from './types/WtcConfig';
|
||||
import { MessageKey, message } from './i18n';
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const { error } = console;
|
||||
|
||||
const input = async (): Promise<WtcPromptResult> => {
|
||||
const { defaults, askInput } = getConfig();
|
||||
const input = async (config: WtcConfig): Promise<WtcPromptResult> => {
|
||||
const msg = message(config.language);
|
||||
const fmtDuration = formatDuration(config.language);
|
||||
const { defaults, askInput } = config;
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
|
@ -29,20 +33,14 @@ const input = async (): Promise<WtcPromptResult> => {
|
|||
|
||||
if (askInput.workDayLength) {
|
||||
const durationAnswer = await rl.question(
|
||||
`How long is your work day today, excluding the lunch break? [${formatDuration(
|
||||
defaults.workDayDuration,
|
||||
true,
|
||||
)}] `,
|
||||
msg(MessageKey.promptWorkDayDuration, fmtDuration(defaults.workDayDuration, true)),
|
||||
);
|
||||
if (durationAnswer !== '') {
|
||||
workDayDuration = parseDuration(durationAnswer);
|
||||
if (workDayDuration.asMinutes() <= 0) {
|
||||
error(
|
||||
chalk.red(
|
||||
`Failed to parse ${durationAnswer} to duration, using default work day duration ${formatDuration(
|
||||
defaults.workDayDuration,
|
||||
true,
|
||||
)}`,
|
||||
msg(MessageKey.parseTimeFailed, durationAnswer, fmtDuration(defaults.workDayDuration)),
|
||||
),
|
||||
);
|
||||
workDayDuration = undefined;
|
||||
|
@ -55,19 +53,11 @@ const input = async (): Promise<WtcPromptResult> => {
|
|||
}
|
||||
|
||||
if (askInput.startTime) {
|
||||
const startTimeAnswer = await rl.question(
|
||||
`What time did you start work today? [${formatTime(defaults.startTime)}] `,
|
||||
);
|
||||
const startTimeAnswer = await rl.question(msg(MessageKey.promptStartTime, formatTime(defaults.startTime)));
|
||||
if (startTimeAnswer !== '') {
|
||||
startedAt = parseTimestamp(startTimeAnswer);
|
||||
if (!startedAt.isValid()) {
|
||||
error(
|
||||
chalk.red(
|
||||
`Failed to parse ${startTimeAnswer} to time, using default start time ${formatTime(
|
||||
defaults.startTime,
|
||||
)}`,
|
||||
),
|
||||
);
|
||||
error(chalk.red(msg(MessageKey.parseTimeFailed, startTimeAnswer, formatTime(defaults.startTime))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,16 +67,13 @@ const input = async (): Promise<WtcPromptResult> => {
|
|||
}
|
||||
|
||||
if (askInput.stopTime) {
|
||||
const stoppedAnswer = await rl.question(
|
||||
`What time did you stop working? [${formatTime(defaults.stopTime)}] `,
|
||||
);
|
||||
const stoppedAnswer = await rl.question(msg(MessageKey.promptStopTime, formatTime(defaults.stopTime)));
|
||||
|
||||
if (stoppedAnswer !== '') {
|
||||
stoppedWorking = true;
|
||||
stoppedAt = parseTimestamp(stoppedAnswer);
|
||||
if (!stoppedAt.isValid()) {
|
||||
error(`Failed to parse ${stoppedAnswer} to time, using current time`);
|
||||
stoppedAt = dayjs();
|
||||
error(chalk.red(msg(MessageKey.parseTimeFailed, stoppedAnswer, formatTime(defaults.stopTime))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,26 +84,25 @@ const input = async (): Promise<WtcPromptResult> => {
|
|||
|
||||
if (stoppedAt.isSame(startedAt) || stoppedAt.isBefore(startedAt)) {
|
||||
error(
|
||||
chalk.red(
|
||||
`Start time (${formatTime(startedAt)}) needs to be before stop time (${formatTime(
|
||||
stoppedAt,
|
||||
)}). Exiting`,
|
||||
),
|
||||
chalk.red(msg(MessageKey.startTimeBeforeStopTimeError, formatTime(startedAt), formatTime(stoppedAt))),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let worked = dayjs.duration(stoppedAt.diff(startedAt));
|
||||
|
||||
const hadLunch =
|
||||
askInput.hadLunch && (await rl.question('Did you have a lunch break? [Y/n] ')).toLowerCase() !== 'n';
|
||||
let hadLunch = false;
|
||||
if (askInput.hadLunch) {
|
||||
const lunchAnswer = (await rl.question(msg(MessageKey.promptLunchBreak))).toLowerCase();
|
||||
hadLunch = lunchAnswer === 'y' || lunchAnswer === 'k';
|
||||
}
|
||||
|
||||
if (hadLunch) {
|
||||
worked = worked.subtract(defaults.lunchBreakDuration);
|
||||
}
|
||||
|
||||
// Calculate unlogged time
|
||||
let loggedAnswer = await rl.question('How many hours did you log already? [00:00] ');
|
||||
let loggedAnswer = await rl.question(msg(MessageKey.promptLogged));
|
||||
if (loggedAnswer === '') {
|
||||
loggedAnswer = '00:00';
|
||||
}
|
||||
|
|
|
@ -1,31 +1,40 @@
|
|||
import chalk from 'chalk';
|
||||
import { formatDuration, formatTimestamp, getHoursRoundedStr } from './format';
|
||||
import { WtcPromptResult } from './types/WtcPromptResult';
|
||||
import { MessageKey, message } from './i18n.js';
|
||||
import WtcConfig from './types/WtcConfig';
|
||||
|
||||
const { log } = console;
|
||||
|
||||
const output = (result: WtcPromptResult) => {
|
||||
const output = (result: WtcPromptResult, config: WtcConfig) => {
|
||||
const msg = message(config.language);
|
||||
const fmtDuration = formatDuration(config.language);
|
||||
const hoursRounded = getHoursRoundedStr(config.language);
|
||||
const { startedAt, stoppedAt, stoppedWorking, worked, unLogged, workLeft, workedOverTime } = result;
|
||||
log();
|
||||
log('Started working at:', formatTimestamp(startedAt));
|
||||
log((stoppedWorking ? 'Stopped working' : 'Hours calculated') + ' at:', formatTimestamp(stoppedAt));
|
||||
log('Worked today:', chalk.green(formatDuration(worked)), chalk.yellow(getHoursRoundedStr(worked)));
|
||||
log(msg(MessageKey.startedWorking), formatTimestamp(startedAt));
|
||||
log(
|
||||
(stoppedWorking ? msg(MessageKey.stoppedWorking) : msg(MessageKey.hoursCalculated)) +
|
||||
` ${msg(MessageKey.klo)}:`,
|
||||
formatTimestamp(stoppedAt),
|
||||
);
|
||||
log(msg(MessageKey.workedToday), chalk.green(fmtDuration(worked)), chalk.yellow(hoursRounded(worked)));
|
||||
|
||||
if (unLogged.asMinutes() == 0) {
|
||||
log('Unlogged today:', chalk.green('none'));
|
||||
} else if (unLogged.asMinutes() > 0) {
|
||||
log('Unlogged today:', chalk.red(formatDuration(unLogged)), chalk.yellow(getHoursRoundedStr(unLogged)));
|
||||
} else if (unLogged.asMinutes() < 0) {
|
||||
const unLoggedMinutes = unLogged.asMinutes();
|
||||
if (unLoggedMinutes >= 0) {
|
||||
log(
|
||||
chalk.red(`You have logged ${formatDuration(unLogged)} more than you worked today!`),
|
||||
chalk.yellow(getHoursRoundedStr(unLogged)),
|
||||
msg(MessageKey.unloggedToday),
|
||||
unLoggedMinutes === 0 ? chalk.green(msg(MessageKey.none)) : chalk.red(fmtDuration(unLogged)),
|
||||
chalk.yellow(hoursRounded(unLogged)),
|
||||
);
|
||||
} else if (unLoggedMinutes < 0) {
|
||||
log(chalk.red(msg(MessageKey.loggedOver, fmtDuration(unLogged))), chalk.yellow(hoursRounded(unLogged)));
|
||||
}
|
||||
|
||||
if (workLeft.asMinutes() > 0) {
|
||||
log('You still have to work', chalk.green(formatDuration(workLeft)), 'more today');
|
||||
log(msg(MessageKey.workLeft, chalk.green(fmtDuration(workLeft))));
|
||||
} else if (workedOverTime) {
|
||||
log('You worked', chalk.green(formatDuration(workedOverTime), 'overtime today'));
|
||||
log(msg(MessageKey.workedOvertime, chalk.green(fmtDuration(workedOverTime))));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
6
src/types/Language.ts
Normal file
6
src/types/Language.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
enum Language {
|
||||
en = 'en',
|
||||
fi = 'fi',
|
||||
}
|
||||
|
||||
export default Language;
|
20
src/types/WtcConfig.ts
Normal file
20
src/types/WtcConfig.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Dayjs } from 'dayjs';
|
||||
import { Duration } from 'dayjs/plugin/duration.js';
|
||||
import Language from './Language.js';
|
||||
|
||||
export default interface WtcConfig {
|
||||
language: Language,
|
||||
defaults: {
|
||||
workDayDuration: Duration;
|
||||
lunchBreakDuration: Duration;
|
||||
startTime: Dayjs;
|
||||
stopTime: Dayjs;
|
||||
};
|
||||
askInput: {
|
||||
workDayLength: boolean;
|
||||
startTime: boolean;
|
||||
stopTime: boolean;
|
||||
logged: boolean;
|
||||
hadLunch: boolean;
|
||||
};
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import getConfig from './config.js';
|
||||
import input from './input.js';
|
||||
import output from './output.js';
|
||||
|
||||
const ui = async () => {
|
||||
const result = await input();
|
||||
output(result);
|
||||
const config = getConfig();
|
||||
const result = await input(config);
|
||||
output(result, config);
|
||||
};
|
||||
|
||||
export default ui;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue