Skip to content

Commit

Permalink
feat: functional style l10n translation
Browse files Browse the repository at this point in the history
Define type-safe message translation in functional programming style.
  • Loading branch information
wdhongtw committed Jan 16, 2025
1 parent d636a05 commit 50ba1b6
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 79 deletions.
69 changes: 38 additions & 31 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ import * as gpg from './indicator/gpg';
import { Logger } from "./manager";
import KeyStatusManager from "./manager";
import { Storage, KeyStatusEvent } from "./manager";
import { m } from "./message";
import * as message from "./message";
import { keys } from "./message";

/** Translate a message which already takes some argument. */
function _t(w: message.WaitTranslate): string {
return w(vscode.l10n.t);
}

type statusStyleEnum = "fingerprintWithUserId" | "fingerprint" | "userId";

const actions = {
YES: vscode.l10n.t(m["actionYes"]),
NO: vscode.l10n.t(m["actionNo"]),
DO_NOT_ASK_AGAIN: vscode.l10n.t(m["actionDoNotAskAgain"]),
OK: vscode.l10n.t(m["actionOK"]),
OPEN_SETTING: vscode.l10n.t(m["actionOpenSetting"]),
YES: _t(keys.actionYes()),
NO: _t(keys.actionNo()),
DO_NOT_ASK_AGAIN: _t(keys.actionDoNotAskAgain()),
OK: _t(keys.actionOK()),
OPEN_SETTING: _t(keys.actionOpenSetting()),
};

function toFolders(folders: readonly vscode.WorkspaceFolder[]): string[] {
Expand All @@ -31,7 +37,7 @@ function toFolders(folders: readonly vscode.WorkspaceFolder[]): string[] {
async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManager: KeyStatusManager): Promise<false | vscode.QuickPickItem[]> {
const list = [...secretStorage];
if (list.length === 0) {
vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase']));
vscode.window.showInformationMessage(_t(keys.noCachedPassphrase()));
return false;
}
const items: vscode.QuickPickItem[] = [];
Expand All @@ -44,7 +50,7 @@ async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManage
const currentKey = currentKeyList.length === 1 ? currentKeyList[0] : undefined;
if (currentKey) {
items.push({
label: vscode.l10n.t(m["currentKey"]),
label: _t(keys.currentKey()),
alwaysShow: true,
kind: vscode.QuickPickItemKind.Separator,
});
Expand All @@ -59,7 +65,7 @@ async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManage
const restList = list.filter((fp: string) => !isCurrentKey(fp));
if (restList.length > 0) {
items.push({
label: vscode.l10n.t(m["restKey"]),
label: _t(keys.restKey()),
alwaysShow: false,
kind: vscode.QuickPickItemKind.Separator,
});
Expand Down Expand Up @@ -94,6 +100,7 @@ export async function activate(context: vscode.ExtensionContext) {

logger.info('Create key status manager');
const keyStatusManager = new KeyStatusManager(
_t,
logger,
new git.CliGit(),
new gpg.CliGpg(logger),
Expand All @@ -113,16 +120,16 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand(commandId, async () => {
const currentKey = keyStatusManager.getCurrentKey();
if (!currentKey) {
vscode.window.showErrorMessage(vscode.l10n.t(m["noKeyInCurrentFolder"]));
vscode.window.showErrorMessage(_t(keys.noKeyInCurrentFolder()));
return;
}
const passphrase = await vscode.window.showInputBox({
prompt: keyStatusManager.enablePassphraseCache
? vscode.l10n.t(m['passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled'])
: vscode.l10n.t(m['passphraseInputPromptTitle']),
? _t(keys.passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled())
: _t(keys.passphraseInputPromptTitle()),
password: true,
placeHolder: currentKey.userId
? vscode.l10n.t(m['keyDescriptionWithUserId'], currentKey.userId)
? _t(keys.keyDescriptionWithUserId(currentKey.userId))
: undefined,
});
if (passphrase === undefined) { return; }
Expand All @@ -131,15 +138,15 @@ export async function activate(context: vscode.ExtensionContext) {
await keyStatusManager.syncStatus();
} catch (err) {
if (err instanceof Error) {
vscode.window.showErrorMessage(vscode.l10n.t(m['keyUnlockFailedWithId'], err.message));
vscode.window.showErrorMessage(_t(keys.keyUnlockFailedWithId(err.message)));
}
}

if (keyStatusManager.enablePassphraseCache) {
await secretStorage.set(currentKey.fingerprint, passphrase);
vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlockedWithCachedPassphrase']));
vscode.window.showInformationMessage(_t(keys.keyUnlockedWithCachedPassphrase()));
} else {
vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlocked']));
vscode.window.showInformationMessage(_t(keys.keyUnlocked()));
await introduceCacheFeature(context);
}
}));
Expand All @@ -149,53 +156,53 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.commands.registerCommand("gpgIndicator.deletePassphraseCache", async () => {
const items: vscode.QuickPickItem[] | false = await generateKeyList(secretStorage, keyStatusManager);
if (!items) {
vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase']));
vscode.window.showInformationMessage(_t(keys.noCachedPassphrase()));
return;
}
const targets = await vscode.window.showQuickPick(items, {
title: vscode.l10n.t(m["cachedPassphraseListForDeletion"]),
title: _t(keys.cachedPassphraseListForDeletion()),
canPickMany: true,
ignoreFocusOut: true,
matchOnDescription: true,
matchOnDetail: true,
placeHolder: vscode.l10n.t(m["cachedPassphraseListForDeletionPlaceHolder"]),
placeHolder: _t(keys.cachedPassphraseListForDeletionPlaceHolder()),
});
if (!Array.isArray(targets) || targets.length === 0) {
return;
}
await Promise.all(targets.map((target) => secretStorage.delete(target.label)));
vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseDeleted']));
vscode.window.showInformationMessage(_t(keys.passphraseDeleted()));
}));

context.subscriptions.push(vscode.commands.registerCommand("gpgIndicator.listPassphraseCache", async () => {
const items: vscode.QuickPickItem[] | false = await generateKeyList(secretStorage, keyStatusManager);
if (!items) {
vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase']));
vscode.window.showInformationMessage(_t(keys.noCachedPassphrase()));
return;
}
/**
* Because of the lack of the listing function, use quick pick instead.
*/
await vscode.window.showQuickPick(items, {
title: vscode.l10n.t(m["cachedPassphraseList"]),
title: _t(keys.cachedPassphraseList()),
});
}));

context.subscriptions.push(vscode.commands.registerCommand("gpgIndicator.clearPassphraseCache", async () => {
if ([...secretStorage].length === 0) {
vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase']));
vscode.window.showInformationMessage(_t(keys.noCachedPassphrase()));
return;
}
if ((await vscode.window.showInformationMessage<vscode.MessageItem>(
vscode.l10n.t(m["passphraseClearanceConfirm"]),
_t(keys.passphraseClearanceConfirm()),
{ modal: true },
{ title: actions.YES },
{ title: actions.NO, isCloseAffordance: true },
))?.title !== actions.YES) {
return;
}
await Promise.all([...secretStorage].map((key) => secretStorage.delete(key)));
vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseCleared']));
vscode.window.showInformationMessage(_t(keys.passphraseCleared()));
}));

const updateKeyStatus = (event?: KeyStatusEvent) => {
Expand Down Expand Up @@ -239,7 +246,7 @@ export async function activate(context: vscode.ExtensionContext) {
if (keyStatusManager.enablePassphraseCache && !newEnablePassphraseCache) {
try {
await Promise.all([...secretStorage].map((key) => secretStorage.delete(key)));
vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseCleared']));
vscode.window.showInformationMessage(_t(keys.passphraseCleared()));
}
catch (e) {
logger.error(`Cannot clear the passphrase cache when "enablePassphraseCache" turn to off: ${e instanceof Error ? e.message : JSON.stringify(e, null, 4)}`);
Expand Down Expand Up @@ -295,7 +302,7 @@ async function introduceCacheFeature(context: vscode.ExtensionContext) {
}

const result = await vscode.window.showInformationMessage<string>(
vscode.l10n.t(m["enableSecurelyPassphraseCacheNotice"]),
_t(keys.enableSecurelyPassphraseCacheNotice()),
actions.YES,
actions.NO,
actions.DO_NOT_ASK_AGAIN,
Expand All @@ -308,17 +315,17 @@ async function introduceCacheFeature(context: vscode.ExtensionContext) {
configuration.update("enablePassphraseCache", true, true);
}

let postMessage: string;
let postMessage: message.WaitTranslate;
if (result === actions.YES) {
postMessage = m["enableSecurelyPassphraseCacheNoticeAgreed"];
postMessage = keys.enableSecurelyPassphraseCacheNoticeAgreed();
} else { // do not ask again case
postMessage = m["enableSecurelyPassphraseCacheNoticeForbidden"];
postMessage = keys.enableSecurelyPassphraseCacheNoticeForbidden();
}
// Due to the fact that vscode automatically collapses ordinary notifications into one line,
// causing `enablePassphraseCache` setting links to be collapsed,
// notifications with options are used instead to avoid being collapsed.
const postMessageResult = await vscode.window.showInformationMessage<string>(
vscode.l10n.t(postMessage),
_t(postMessage),
actions.OK,
actions.OPEN_SETTING,
);
Expand Down
24 changes: 13 additions & 11 deletions src/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import * as vscode from 'vscode';

import * as process from './indicator/process';
import { Mutex } from "./indicator/locker";
import { m } from "./message";
import * as message from "./message";
import { keys } from "./message";

/**
* Logger is a sample interface for basic logging ability.
Expand Down Expand Up @@ -66,6 +67,7 @@ export default class KeyStatusManager {
* @param syncInterval - key status sync interval in seconds.
*/
constructor(
private _t: (w: message.WaitTranslate) => string,
private logger: Logger,
private git: GitAdapter,
private gpg: GpgAdapter,
Expand Down Expand Up @@ -94,10 +96,10 @@ export default class KeyStatusManager {
this.syncInterval = syncInterval;
}

private show(isChanged: boolean, changedMsg: string, defaultMsg: string) {
private show(isChanged: boolean, changedMsg: message.WaitTranslate, defaultMsg: message.WaitTranslate) {
vscode.window.showInformationMessage(isChanged
? vscode.l10n.t(changedMsg)
: vscode.l10n.t(defaultMsg),
? this._t(changedMsg)
: this._t(defaultMsg),
);
}

Expand Down Expand Up @@ -168,9 +170,9 @@ export default class KeyStatusManager {
try {
await this.unlockCurrentKey(passphrase);
if (isUnlockedPrev) {
this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyRelockedAndAutomaticallyUnlocked']);
this.show(isChanged, keys.keyChangedAndAutomaticallyUnlocked(), keys.keyRelockedAndAutomaticallyUnlocked());
} else {
this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyAutomaticallyUnlocked']);
this.show(isChanged, keys.keyChangedAndAutomaticallyUnlocked(), keys.keyAutomaticallyUnlocked());
}
} catch (err) {
if (!(err instanceof Error)) {
Expand All @@ -179,9 +181,9 @@ export default class KeyStatusManager {
this.logger.error(`Cannot unlock the key with the cached passphrase: ${err.message}`);
await this.secretStorage.delete(keyInfo.fingerprint);
if (isUnlockedPrev) {
this.show(isChanged, m['keyChangedButAutomaticallyUnlockFailed'], m['keyRelockedButAutomaticallyUnlockFailed']);
this.show(isChanged, keys.keyChangedButAutomaticallyUnlockFailed(), keys.keyRelockedButAutomaticallyUnlockFailed());
} else {
this.show(isChanged, m['keyChangedButAutomaticallyUnlockFailed'], m['keyAutomaticallyUnlockFailed']);
this.show(isChanged, keys.keyChangedButAutomaticallyUnlockFailed(), keys.keyAutomaticallyUnlockFailed());
}
}

Expand All @@ -197,7 +199,7 @@ export default class KeyStatusManager {
private async showInfoOnly(isChanged: boolean, isUnlockedPrev: boolean, keyInfo: GpgKeyInfo): Promise<boolean> {
const isUnlocked = await this.gpg.isKeyUnlocked(keyInfo.keygrip);
if (isUnlockedPrev && !isUnlocked) {
this.show(isChanged, m['keyChanged'], m['keyRelocked']);
this.show(isChanged, keys.keyChanged(), keys.keyRelocked());
}

return isUnlocked;
Expand Down Expand Up @@ -279,11 +281,11 @@ export default class KeyStatusManager {
// Lock or unlock current key
async unlockCurrentKey(passphrase: string): Promise<void> {
if (this.activateFolder === undefined) {
throw new Error(vscode.l10n.t(m['noActiveFolder']));
throw new Error(this._t(keys.noActiveFolder()));
}

if (this.currentKey === undefined) {
throw new Error(vscode.l10n.t(m['noKeyForCurrentFolder']));
throw new Error(this._t(keys.noKeyForCurrentFolder()));
}

if (await this.gpg.isKeyUnlocked(this.currentKey.keygrip)) {
Expand Down
Loading

0 comments on commit 50ba1b6

Please sign in to comment.