Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Strings extract #165

Merged
merged 10 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ Strings for autocompletion are loaded during the bootstrap of VSCode and are als

This gif illustrates the autocompletion of string keys in a React project that uses the [Lingui](https://lingui.dev/tools/crowdin) library.

### String extraction

The plugin allows you to extract strings from your source files and upload them to Crowdin. Just select the necessary text and use the `"Crowdin: Extract String"` from the context menu. You will then be asked to enter the key for that string and select the file to upload it to.

### Command Palette commands

The plugin provides the following commands in the VS Code [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-) (<kbd>Shift</kbd>+<kbd>Command</kbd>+<kbd>P</kbd>/<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>P</kbd>):
Expand Down
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
]
},
"commands": [
{
"command": "string.extract",
"enablement": "editorHasSelection && !editorHasMultipleSelections",
"title": "Crowdin: Extract string"
},
{
"command": "crowdin.signIn",
"title": "Sign in",
Expand Down Expand Up @@ -214,6 +219,12 @@
}
],
"menus": {
"editor/context": [
{
"command": "string.extract",
"when": "editorHasSelection && !editorHasMultipleSelections"
}
],
"view/title": [
{
"command": "download.refresh",
Expand Down
64 changes: 64 additions & 0 deletions src/client/crowdinClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ https.globalAgent.options.rejectUnauthorized = false;

export class CrowdinClient {
readonly crowdin: Crowdin;
readonly crowdinWithouRetry: Crowdin;
readonly projectId: number;
readonly branch?: string;

Expand All @@ -34,6 +35,9 @@ export class CrowdinClient {
waitInterval: Constants.CLIENT_RETRY_WAIT_INTERVAL_MS,
},
});
this.crowdinWithouRetry = new Crowdin(credentials, {
userAgent: `crowdin-vscode-plugin/${Constants.PLUGIN_VERSION} vscode/${Constants.VSCODE_VERSION}`,
});
}

get crowdinBranch(): { name: string; title: string } | undefined {
Expand All @@ -53,6 +57,66 @@ export class CrowdinClient {
}
}

async listFiles() {
let branchId: number | undefined;
const branch = this.crowdinBranch;

if (this.stringsBased) {
if (!branch) {
throw new Error('Branch is not specified');
}
}

if (branch) {
const branches = await this.crowdinWithouRetry.sourceFilesApi.listProjectBranches(this.projectId, {
name: branch.name,
});
branchId = branches.data.find((e) => e.data.name === branch.name)?.data.id;

if (!branchId) {
throw new Error(`Failed to find branch with name ${branch.name}`);
}
}

const files = await this.crowdinWithouRetry.sourceFilesApi
.withFetchAll()
.listProjectFiles(this.projectId, { branchId });

return files.data.map((f) => f.data);
}

async addString({ text, id, fileId }: { text: string; id: string; fileId?: number }) {
if (!this.stringsBased) {
await this.crowdinWithouRetry.sourceStringsApi.addString(this.projectId, {
text,
identifier: id,
fileId,
});
return;
}

const branch = this.crowdinBranch;

if (!branch) {
throw new Error('Branch is not specified');
}

const branches = await this.crowdinWithouRetry.sourceFilesApi.listProjectBranches(this.projectId, {
name: branch.name,
});
const branchId = branches.data.find((e) => e.data.name === branch.name)?.data.id;

if (!branchId) {
throw new Error(`Failed to find branch with name ${branch.name}`);
}

await this.crowdinWithouRetry.sourceStringsApi.addString(this.projectId, {
branchId,
text,
identifier: id,
});
}

async getStrings() {
const strings = await this.crowdin.sourceStringsApi.withFetchAll().listProjectStrings(this.projectId);
return strings.data.map((str) => str.data);
Expand Down
19 changes: 19 additions & 0 deletions src/commands/createConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as vscode from 'vscode';
import { ConfigProvider } from '../config/configProvider';
import { Constants } from '../constants';
import { CommonUtil } from '../util/commonUtil';

export async function createConfig() {
const workspace = await CommonUtil.getWorkspace();
if (!workspace) {
vscode.window.showWarningMessage('Project workspace is empty');
return;
}
const configProvider = new ConfigProvider(workspace);
const { file, isNew } = await configProvider.create();
vscode.commands.executeCommand('setContext', 'crowdinConfigExists', true);
vscode.commands.executeCommand(Constants.VSCODE_OPEN_FILE, vscode.Uri.file(file));
if (isNew) {
await Constants.CONFIG_HOLDER.load();
}
}
117 changes: 117 additions & 0 deletions src/commands/extractString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ProjectsGroupsModel } from '@crowdin/crowdin-api-client';
import * as vscode from 'vscode';
import { buildClient } from '../config/configModel';
import { Constants } from '../constants';
import { CommonUtil } from '../util/commonUtil';
import { ErrorHandler } from '../util/errorHandler';

const editableFileTypes = [
'csv',
'resx',
'json',
'i18next_json',
'android',
'macosx',
'strings',
'properties',
'xliff',
'arb',
'gettext',
'yaml',
'xlsx',
];

export async function extractString() {
CommonUtil.withProgress(async () => {
try {
await addString();
} catch (err) {
const message = ErrorHandler.getMessage(err);
vscode.window.showErrorMessage(`Crowdin: ${message}`);
}
}, 'Extracting string...');
}

async function addString() {
const editor = vscode.window.activeTextEditor;
const selection = editor?.selection;
if (!selection || selection.isEmpty) {
return;
}

const selectionRange = new vscode.Range(
selection.start.line,
selection.start.character,
selection.end.line,
selection.end.character
);
const text = editor.document.getText(selectionRange).trim();

if (!text.length) {
return;
}

const currentWorkspace = await CommonUtil.getWorkspace();
if (!currentWorkspace) {
vscode.window.showWarningMessage('Project workspace is empty');
return;
}

const config = (await Constants.CONFIG_HOLDER.configurations()).find(
([, workspace]) => workspace === currentWorkspace
);

if (!config) {
vscode.window.showWarningMessage('Project configuration file is missing');
return;
}

const isStringsBased = config[0].project.type === ProjectsGroupsModel.Type.STRINGS_BASED;
const client = buildClient(currentWorkspace.uri, config[0].config, isStringsBased);

const identifier = await vscode.window.showInputBox({ title: 'Please enter a string identifier' });

if (!identifier?.trim()?.length) {
vscode.window.showErrorMessage('Please enter string identifier');
return;
}

let fileId;

if (!isStringsBased) {
const files = await client.listFiles();
const allowedFiles = files.filter((f) => editableFileTypes.includes(f.type));

const file = await vscode.window.showQuickPick(
allowedFiles.map(
(e) =>
({
label: e.name,
description: e.path,
detail: e.id.toString(),
} as vscode.QuickPickItem)
),
{
canPickMany: false,
title: 'Please select a file',
}
);

if (!file) {
vscode.window.showErrorMessage('Please select a file');
return;
}

fileId = Number(file.detail);
}

await client.addString({
fileId,
id: identifier,
text,
});

vscode.window.showInformationMessage('String successfully extracted to Crowdin');

editor.edit((editBuilder) => editBuilder.replace(selectionRange, identifier));
}
19 changes: 19 additions & 0 deletions src/commands/openConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as vscode from 'vscode';
import { ConfigProvider } from '../config/configProvider';
import { Constants } from '../constants';
import { CommonUtil } from '../util/commonUtil';

export async function openConfig() {
const workspace = await CommonUtil.getWorkspace();
if (!workspace) {
vscode.window.showWarningMessage('Project workspace is empty');
return;
}
const configProvider = new ConfigProvider(workspace);
const file = await configProvider.getFile();
if (!file) {
vscode.window.showWarningMessage(`Could not find configuration file in ${workspace.name}`);
return;
}
vscode.commands.executeCommand(Constants.VSCODE_OPEN_FILE, vscode.Uri.file(file));
}
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as vscode from 'vscode';
import { CrowdinConfigHolder } from './plugin/crowdinConfigHolder';

export class Constants {
static EXTENSION_CONTEXT: vscode.ExtensionContext;
static CONFIG_HOLDER: CrowdinConfigHolder;
//tree providers
static readonly UPLOAD = 'upload';
static readonly DOWNLOAD = 'download';
Expand All @@ -27,6 +29,7 @@ export class Constants {
static readonly SIGN_IN_COMMAND = 'crowdin.signIn';
static readonly SIGN_OUT_COMMAND = 'crowdin.signOut';
static readonly SELECT_PROJECT_COMMAND = 'crowdin.selectProject';
static readonly STRING_EXTRACT_COMMAND = 'string.extract';
//properties
static readonly AUTO_REFRESH_PROPERTY = 'crowdin.autoRefresh';
static readonly STRINGS_COMPLETION_PROPERTY = 'crowdin.stringsCompletion';
Expand All @@ -43,5 +46,6 @@ export class Constants {
static initialize(context: vscode.ExtensionContext) {
Constants.VSCODE_VERSION = vscode.version;
Constants.EXTENSION_CONTEXT = context;
Constants.CONFIG_HOLDER = new CrowdinConfigHolder();
}
}
Loading
Loading