diff --git a/src/extension.ts b/src/extension.ts index 063fedf..9301f96 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,16 +8,21 @@ 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 m from "./message"; + +/** Translate a message which already takes some argument. */ +function _t(w: m.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(m.actionYes()), + NO: _t(m.actionNo()), + DO_NOT_ASK_AGAIN: _t(m.actionDoNotAskAgain()), + OK: _t(m.actionOK()), + OPEN_SETTING: _t(m.actionOpenSetting()), }; function toFolders(folders: readonly vscode.WorkspaceFolder[]): string[] { @@ -31,7 +36,7 @@ function toFolders(folders: readonly vscode.WorkspaceFolder[]): string[] { async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManager: KeyStatusManager): Promise { const list = [...secretStorage]; if (list.length === 0) { - vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase'])); + vscode.window.showInformationMessage(_t(m.noCachedPassphrase())); return false; } const items: vscode.QuickPickItem[] = []; @@ -44,7 +49,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(m.currentKey()), alwaysShow: true, kind: vscode.QuickPickItemKind.Separator, }); @@ -59,7 +64,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(m.restKey()), alwaysShow: false, kind: vscode.QuickPickItemKind.Separator, }); @@ -94,6 +99,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), @@ -113,16 +119,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(m.noKeyInCurrentFolder())); return; } const passphrase = await vscode.window.showInputBox({ prompt: keyStatusManager.enablePassphraseCache - ? vscode.l10n.t(m['passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled']) - : vscode.l10n.t(m['passphraseInputPromptTitle']), + ? _t(m.passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled()) + : _t(m.passphraseInputPromptTitle()), password: true, placeHolder: currentKey.userId - ? vscode.l10n.t(m['keyDescriptionWithUserId'], currentKey.userId) + ? _t(m.keyDescriptionWithUserId(currentKey.userId)) : undefined, }); if (passphrase === undefined) { return; } @@ -131,15 +137,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(m.keyUnlockFailedWithId(err.message))); } } if (keyStatusManager.enablePassphraseCache) { await secretStorage.set(currentKey.fingerprint, passphrase); - vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlockedWithCachedPassphrase'])); + vscode.window.showInformationMessage(_t(m.keyUnlockedWithCachedPassphrase())); } else { - vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlocked'])); + vscode.window.showInformationMessage(_t(m.keyUnlocked())); await introduceCacheFeature(context); } })); @@ -149,45 +155,45 @@ 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(m.noCachedPassphrase())); return; } const targets = await vscode.window.showQuickPick(items, { - title: vscode.l10n.t(m["cachedPassphraseListForDeletion"]), + title: _t(m.cachedPassphraseListForDeletion()), canPickMany: true, ignoreFocusOut: true, matchOnDescription: true, matchOnDetail: true, - placeHolder: vscode.l10n.t(m["cachedPassphraseListForDeletionPlaceHolder"]), + placeHolder: _t(m.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(m.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(m.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(m.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(m.noCachedPassphrase())); return; } if ((await vscode.window.showInformationMessage( - vscode.l10n.t(m["passphraseClearanceConfirm"]), + _t(m.passphraseClearanceConfirm()), { modal: true }, { title: actions.YES }, { title: actions.NO, isCloseAffordance: true }, @@ -195,7 +201,7 @@ export async function activate(context: vscode.ExtensionContext) { return; } await Promise.all([...secretStorage].map((key) => secretStorage.delete(key))); - vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseCleared'])); + vscode.window.showInformationMessage(_t(m.passphraseCleared())); })); const updateKeyStatus = (event?: KeyStatusEvent) => { @@ -239,7 +245,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(m.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)}`); @@ -295,7 +301,7 @@ async function introduceCacheFeature(context: vscode.ExtensionContext) { } const result = await vscode.window.showInformationMessage( - vscode.l10n.t(m["enableSecurelyPassphraseCacheNotice"]), + _t(m.enableSecurelyPassphraseCacheNotice()), actions.YES, actions.NO, actions.DO_NOT_ASK_AGAIN, @@ -308,17 +314,17 @@ async function introduceCacheFeature(context: vscode.ExtensionContext) { configuration.update("enablePassphraseCache", true, true); } - let postMessage: string; + let postMessage: m.WaitTranslate; if (result === actions.YES) { - postMessage = m["enableSecurelyPassphraseCacheNoticeAgreed"]; + postMessage = m.enableSecurelyPassphraseCacheNoticeAgreed(); } else { // do not ask again case - postMessage = m["enableSecurelyPassphraseCacheNoticeForbidden"]; + postMessage = m.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( - vscode.l10n.t(postMessage), + _t(postMessage), actions.OK, actions.OPEN_SETTING, ); diff --git a/src/manager.ts b/src/manager.ts index e2d855e..f0c5552 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as process from './indicator/process'; import { Mutex } from "./indicator/locker"; -import { m } from "./message"; +import * as m from "./message"; /** * Logger is a sample interface for basic logging ability. @@ -66,6 +66,7 @@ export default class KeyStatusManager { * @param syncInterval - key status sync interval in seconds. */ constructor( + private _t: (w: m.WaitTranslate) => string, private logger: Logger, private git: GitAdapter, private gpg: GpgAdapter, @@ -94,10 +95,10 @@ export default class KeyStatusManager { this.syncInterval = syncInterval; } - private show(isChanged: boolean, changedMsg: string, defaultMsg: string) { + private show(isChanged: boolean, changedMsg: m.WaitTranslate, defaultMsg: m.WaitTranslate) { vscode.window.showInformationMessage(isChanged - ? vscode.l10n.t(changedMsg) - : vscode.l10n.t(defaultMsg), + ? this._t(changedMsg) + : this._t(defaultMsg), ); } @@ -168,9 +169,9 @@ export default class KeyStatusManager { try { await this.unlockCurrentKey(passphrase); if (isUnlockedPrev) { - this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyRelockedAndAutomaticallyUnlocked']); + this.show(isChanged, m.keyChangedAndAutomaticallyUnlocked(), m.keyRelockedAndAutomaticallyUnlocked()); } else { - this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyAutomaticallyUnlocked']); + this.show(isChanged, m.keyChangedAndAutomaticallyUnlocked(), m.keyAutomaticallyUnlocked()); } } catch (err) { if (!(err instanceof Error)) { @@ -179,9 +180,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, m.keyChangedButAutomaticallyUnlockFailed(), m.keyRelockedButAutomaticallyUnlockFailed()); } else { - this.show(isChanged, m['keyChangedButAutomaticallyUnlockFailed'], m['keyAutomaticallyUnlockFailed']); + this.show(isChanged, m.keyChangedButAutomaticallyUnlockFailed(), m.keyAutomaticallyUnlockFailed()); } } @@ -197,7 +198,7 @@ export default class KeyStatusManager { private async showInfoOnly(isChanged: boolean, isUnlockedPrev: boolean, keyInfo: GpgKeyInfo): Promise { const isUnlocked = await this.gpg.isKeyUnlocked(keyInfo.keygrip); if (isUnlockedPrev && !isUnlocked) { - this.show(isChanged, m['keyChanged'], m['keyRelocked']); + this.show(isChanged, m.keyChanged(), m.keyRelocked()); } return isUnlocked; @@ -279,11 +280,11 @@ export default class KeyStatusManager { // Lock or unlock current key async unlockCurrentKey(passphrase: string): Promise { if (this.activateFolder === undefined) { - throw new Error(vscode.l10n.t(m['noActiveFolder'])); + throw new Error(this._t(m.noActiveFolder())); } if (this.currentKey === undefined) { - throw new Error(vscode.l10n.t(m['noKeyForCurrentFolder'])); + throw new Error(this._t(m.noKeyForCurrentFolder())); } if (await this.gpg.isKeyUnlocked(this.currentKey.keygrip)) { diff --git a/src/message.ts b/src/message.ts index d653a6d..671d934 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,38 +1,99 @@ -export const m = { - "actionYes": "Yes", - "actionNo": "No", - "actionDoNotAskAgain": "Don't ask again", - "actionOK": "OK", - "actionOpenSetting": "Open setting", - "separator": ", ", - "noKeyInCurrentFolder": "Unable to retrieve any key in current folder.", - "passphraseInputPromptTitle": "Input the passphrase for the signing key", - "passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled": "Input the passphrase for the signing key, passphrase cache enabled", - "keyDescriptionWithUserId": "For the key associated with {0}", - "keyUnlockedWithCachedPassphrase": "Key unlocked, and the passphrase is stored in the SecretStorage of VSCode.", - "keyUnlocked": "Key unlocked.", - "keyUnlockFailedWithId": "Failed to unlock: {0}", - "noCachedPassphraseForCurrentKey": "There is no cached passphrase for your current key.", - "cachedPassphraseListForDeletion": "List of keys with stored passphrases, select to delete.", - "cachedPassphraseListForDeletionPlaceHolder": "You can search the fingerprint and user id in this search box.", - "passphraseDeleted": "Your cached passphrase has been deleted.", - "noCachedPassphrase": "There is no cached passphrase.", - "currentKey": "Current key", - "restKey": "Rest keys", - "cachedPassphraseList": "List of keys with stored passphrases:", - "passphraseClearanceConfirm": "Do you really want to clear all your cached passphrase? This action CANNOT be reverted.", - "passphraseCleared": "All Your cached passphrase has been cleared.", - "keyChangedAndAutomaticallyUnlocked": "Key changed, and unlocked automatically using the previous-used stored passphrase.", - "keyRelockedAndAutomaticallyUnlocked": "Key re-locked, and unlocked automatically using the previous-used stored passphrase.", - "keyAutomaticallyUnlocked": "Key unlocked automatically using the previous-used stored passphrase.", - "keyChangedButAutomaticallyUnlockFailed": "Key changed, but the previous-used stored passphrase for this key is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.", - "keyRelockedButAutomaticallyUnlockFailed": "Key re-locked automatically, but the previous-used stored passphrase is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.", - "keyAutomaticallyUnlockFailed": "The previous-used stored passphrase is unable to unlock current key, so the passphrase has been deleted and you need to unlock the key manually.", - "keyChanged": "Key changed.", - "keyRelocked": "Key re-locked.", - "noActiveFolder": "No active folder", - "noKeyForCurrentFolder": "No key for current folder", - "enableSecurelyPassphraseCacheNotice": "GPG Indicator come with new passphrase cache feature, would you like to enable this feature?", - "enableSecurelyPassphraseCacheNoticeForbidden": "OK, you can configure it later in setting.", - "enableSecurelyPassphraseCacheNoticeAgreed": "OK, this feature has been enabled, you can configure it later in setting." -}; + +export const actionYes = key0("Yes"); +export const actionNo = key0("No"); +export const actionDoNotAskAgain = key0("Don't ask again"); +export const actionOK = key0("OK"); +export const actionOpenSetting = key0("Open setting"); +export const separator = key0(", "); +export const noKeyInCurrentFolder = key0("Unable to retrieve any key in current folder."); +export const passphraseInputPromptTitle = key0("Input the passphrase for the signing key"); +export const passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled = key0("Input the passphrase for the signing key, passphrase cache enabled"); +export const keyDescriptionWithUserId = key1("For the key associated with {0}"); +export const keyUnlockedWithCachedPassphrase = key0("Key unlocked, and the passphrase is stored in the SecretStorage of VSCode."); +export const keyUnlocked = key0("Key unlocked."); +export const keyUnlockFailedWithId = key1("Failed to unlock: {0}"); +export const noCachedPassphraseForCurrentKey = key0("There is no cached passphrase for your current key."); +export const cachedPassphraseListForDeletion = key0("List of keys with stored passphrases, select to delete."); +export const cachedPassphraseListForDeletionPlaceHolder = key0("You can search the fingerprint and user id in this search box."); +export const passphraseDeleted = key0("Your cached passphrase has been deleted."); +export const noCachedPassphrase = key0("There is no cached passphrase."); +export const currentKey = key0("Current key"); +export const restKey = key0("Rest keys"); +export const cachedPassphraseList = key0("List of keys with stored passphrases:"); +export const passphraseClearanceConfirm = key0("Do you really want to clear all your cached passphrase? This action CANNOT be reverted."); +export const passphraseCleared = key0("All Your cached passphrase has been cleared."); +export const keyChangedAndAutomaticallyUnlocked = key0("Key changed, and unlocked automatically using the previous-used stored passphrase."); +export const keyRelockedAndAutomaticallyUnlocked = key0("Key re-locked, and unlocked automatically using the previous-used stored passphrase."); +export const keyAutomaticallyUnlocked = key0("Key unlocked automatically using the previous-used stored passphrase."); +export const keyChangedButAutomaticallyUnlockFailed = key0("Key changed, but the previous-used stored passphrase for this key is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually."); +export const keyRelockedButAutomaticallyUnlockFailed = key0("Key re-locked automatically, but the previous-used stored passphrase is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually."); +export const keyAutomaticallyUnlockFailed = key0("The previous-used stored passphrase is unable to unlock current key, so the passphrase has been deleted and you need to unlock the key manually."); +export const keyChanged = key0("Key changed."); +export const keyRelocked = key0("Key re-locked."); +export const noActiveFolder = key0("No active folder"); +export const noKeyForCurrentFolder = key0("No key for current folder"); +export const enableSecurelyPassphraseCacheNotice = key0("GPG Indicator come with passphrase cache feature, would you like to enable this feature?"); +export const enableSecurelyPassphraseCacheNoticeForbidden = key0("OK, you can configure it later in setting."); +export const enableSecurelyPassphraseCacheNoticeAgreed = key0("OK, this feature has been enabled, you can configure it later in setting."); + + +/** Formatter models the translator function like `vscode.l10n.t`.*/ +export interface Translator { + (message: string, ...args: Array): string +} + +/** WithTranslator models a string key (with potential arguments) for translation. */ +export interface WaitTranslate { + (translator: Translator): string +} + +export interface Take0 { + (): WaitTranslate +} + +export interface Take1 { + (arg0: string): WaitTranslate +} + +export interface Take2 { + (arg0: string, arg1: string): WaitTranslate +} + +export interface Take3 { + (arg0: string, arg1: string, arg2: string): WaitTranslate +} + +export function key0(message: string): Take0 { + + return () => { + return (translator: Translator) => translator(message); + }; +} + +export function key1(message: string): Take1 { + if (message.indexOf('{0}') === -1) { new Error("invalid key1 message."); } + + return (arg0: string) => { + return (translator: Translator) => translator(message, arg0); + }; +} + +export function key2(message: string): Take2 { + if (message.indexOf('{0}') === -1) { new Error("invalid key2 message."); } + if (message.indexOf('{1}') === -1) { new Error("invalid key2 message."); } + + return (arg0: string, arg1: string) => { + return (translator: Translator) => translator(message, arg0, arg1); + }; +} + +export function key3(message: string): Take3 { + if (message.indexOf('{0}') === -1) { new Error("invalid key3 message."); } + if (message.indexOf('{1}') === -1) { new Error("invalid key3 message."); } + if (message.indexOf('{2}') === -1) { new Error("invalid key3 message."); } + + return (arg0: string, arg1: string, arg2: string) => { + return (translator: Translator) => translator(message, arg0, arg1, arg2); + }; +} +