Skip to content

Commit 509d121

Browse files
committed
Implement language switcher
1 parent 418b537 commit 509d121

File tree

10 files changed

+162
-32
lines changed

10 files changed

+162
-32
lines changed

.github/workflows/test.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ jobs:
3030
steps:
3131
- uses: actions/checkout@v3
3232
- uses: ./
33+
with:
34+
file: __tests__/files/README.md
35+
destination: __tests__/files/docs
3336
env:
3437
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
3538
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
3639

3740
- name: List files
38-
run: ls -la
41+
run: |
42+
cd ./__tests__/files/docs
43+
ls -la
44+
cat README.fr-FR.md

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ To enable it, add the following placeholders to your `README.md` file:
104104

105105
## Options
106106

107-
| Option | Default value | Description |
108-
|------------------------|---------------------------------------|--------------------------------------------------------|
109-
| `file` | `README.md` | The Readme file name |
110-
| `destination` | `./` | A directory where the localized readmes will be placed |
111-
| `languages` | All Crowdin project languages | A list of languages to translate the Readme |
107+
| Option | Default value | Description |
108+
|---------------------|-------------------------------|----------------------------------------------------------|
109+
| `file` | `README.md` | The Readme file name |
110+
| `destination` | `./` | A directory where the localized readmes will be placed |
111+
| `languages` | All Crowdin project languages | A list of languages to translate the Readme |
112+
| `language_switcher` | `false` | Defines whether to add a language switcher to the Readme |
112113

113114
## Contributing
114115

__tests__/files/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Demo Readme
2+
3+
Some super description
4+
5+
## Languages
6+
7+
<!-- README-TRANSLATE-LANGUAGES-START -->
8+
<!-- README-TRANSLATE-LANGUAGES-END -->
9+
10+
## Usage
11+
12+
Usage instructions
13+
14+
## License
15+
16+
MIT

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ inputs:
1717
languages:
1818
required: false
1919
description: 'A list of languages to translate the Readme'
20+
language_switcher:
21+
required: false
22+
description: 'Defines whether to add a language switcher to the Readme'
23+
default: 'false'
2024
runs:
2125
using: 'node16'
2226
main: 'dist/index.js'

dist/index.js

Lines changed: 53 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface Config {
99
destination: string;
1010
branch?: string;
1111
languages?: string[];
12+
languageSwitcher?: boolean;
1213
}
1314

1415
export {CredentialsConfig, Config};

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ async function run(): Promise<void> {
99
file: core.getInput('file'),
1010
branch: core.getInput('branch'),
1111
destination: core.getInput('destination'),
12-
languages: core.getMultilineInput('languages')
12+
languages: core.getMultilineInput('languages'),
13+
languageSwitcher: core.getBooleanInput('language_switcher')
1314
};
1415

1516
let credentialsConfig: CredentialsConfig = {

src/translator.ts

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import crowdin, {SourceFilesModel, TranslationsModel} from '@crowdin/crowdin-api
33
import {Config, CredentialsConfig} from './config';
44
import {Logger} from './logger';
55
import BuildRequest = TranslationsModel.BuildRequest;
6-
import {downloadZipAndUnzip} from './utils';
6+
import {baseName, downloadZipAndUnzip} from './utils';
77

88
export class Translator {
9+
private placeholderStart = '<!-- README-TRANSLATE-LANGUAGES-START -->';
10+
private placeholderEnd = '<!-- README-TRANSLATE-LANGUAGES-END -->';
11+
912
private credentials: CredentialsConfig;
1013
private config: Config;
1114
private logger: Logger;
@@ -25,11 +28,21 @@ export class Translator {
2528
public async translate(): Promise<void> {
2629
this.logger.log('info', 'Start...');
2730

31+
const switcher = await this.renderSwitcher();
32+
33+
if (this.config.languageSwitcher) {
34+
await this.addLanguageSwitcher(this.config.file, switcher);
35+
}
36+
2837
await this.uploadSources();
2938

3039
const translationsUrl = await this.downloadTranslations();
3140

32-
await downloadZipAndUnzip(translationsUrl, this.config.destination);
41+
const translationFiles = await downloadZipAndUnzip(translationsUrl, this.config.destination);
42+
43+
translationFiles.map(async (file: string): Promise<void> => {
44+
await this.addLanguageSwitcher(file, switcher);
45+
});
3346

3447
this.logger.log('info', 'Done!');
3548
}
@@ -38,8 +51,10 @@ export class Translator {
3851
this.logger.log('info', 'Uploading sources...');
3952

4053
const readmeFile = await this.getFile();
54+
const exportPattern = '/README.%locale%.md';
55+
4156
const storageFile = await this.crowdin.uploadStorageApi.addStorage(
42-
this.config.file,
57+
baseName(this.config.file),
4358
fs.readFileSync(this.config.file)
4459
);
4560

@@ -65,25 +80,61 @@ export class Translator {
6580
await this.crowdin.sourceFilesApi.updateOrRestoreFile(this.credentials.projectId, readmeFile.id, {
6681
storageId: storageFile.data.id,
6782
exportOptions: {
68-
exportPattern: '/README.%locale%.md'
83+
exportPattern: exportPattern
6984
}
7085
});
7186

7287
this.logger.log('info', 'Source file successfully updated!');
7388
} else {
7489
await this.crowdin.sourceFilesApi.createFile(this.credentials.projectId, {
7590
storageId: storageFile.data.id,
76-
name: this.config.file,
91+
name: baseName(this.config.file),
7792
branchId: branchId,
7893
exportOptions: {
79-
exportPattern: '/README.%locale%.md'
94+
exportPattern: exportPattern
8095
}
8196
});
8297

8398
this.logger.log('info', 'Source file successfully created!');
8499
}
85100
}
86101

102+
private async addLanguageSwitcher(file: string, switcher: string): Promise<void> {
103+
this.logger.log('info', `Adding language switcher to ${file}...`);
104+
105+
let fileContents = fs.readFileSync(file).toString();
106+
107+
if (!fileContents.includes(this.placeholderStart) || !fileContents.includes(this.placeholderEnd)) {
108+
this.logger.log(
109+
'warning',
110+
`Skipped! Please add ${this.placeholderStart} and ${this.placeholderEnd} to your README.md`
111+
);
112+
113+
return;
114+
}
115+
116+
const sliceFrom = fileContents.indexOf(this.placeholderStart) + this.placeholderStart.length;
117+
const sliceTo = fileContents.indexOf(this.placeholderEnd);
118+
119+
fileContents = `${fileContents.slice(0, sliceFrom)}\n${switcher}\n${fileContents.slice(sliceTo)}`;
120+
121+
this.logger.log('debug', fileContents);
122+
123+
fs.writeFileSync(file, fileContents);
124+
}
125+
126+
private async renderSwitcher(): Promise<string> {
127+
const project = await this.crowdin.projectsGroupsApi.getProject(this.credentials.projectId);
128+
129+
let languages: string[] = [];
130+
131+
project.data.targetLanguages.map(language => {
132+
languages.push(`[${language.name}](${this.config.destination}/README.${language.locale}.md)`);
133+
});
134+
135+
return languages.join(' | ');
136+
}
137+
87138
private async downloadTranslations(): Promise<string> {
88139
this.logger.log('info', 'Downloading translations...');
89140

@@ -112,8 +163,6 @@ export class Translator {
112163
result.data.id
113164
);
114165

115-
this.logger.log('info', `Url: ${translations.data.url}`);
116-
117166
this.logger.log('info', 'Translations downloaded!');
118167

119168
return translations.data.url;
@@ -136,10 +185,12 @@ export class Translator {
136185
}
137186

138187
private getFilePath(): string {
188+
const fileName = baseName(this.config.file);
189+
139190
if (this.config.branch) {
140-
return `/${this.config.branch}/${this.config.file}`;
191+
return `/${this.config.branch}/${fileName}`;
141192
} else {
142-
return `/${this.config.file}`;
193+
return `/${fileName}`;
143194
}
144195
}
145196
}

src/utils.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,24 @@ import AdmZip from 'adm-zip';
33
import axios from 'axios';
44
import {toPlatformPath} from '@actions/core';
55

6-
const downloadZipAndUnzip = async (zipUrl: string, destination: string): Promise<void> => {
6+
const downloadZipAndUnzip = async (zipUrl: string, destination: string): Promise<string[]> => {
77
const response = await axios.get(zipUrl, {responseType: 'arraybuffer'});
8-
const zipFilePath = toPlatformPath(`${destination}/translations-${Date.now()}.zip`);
8+
const zipFilePath = toPlatformPath(`Translations-${Date.now()}.zip`);
99

1010
fs.writeFileSync(zipFilePath, response.data);
1111

1212
const zip = new AdmZip(zipFilePath);
1313
zip.extractAllTo(toPlatformPath(destination), true);
1414

15+
const entries: string[] = zip.getEntries().map(entry => entry.entryName);
16+
1517
fs.unlinkSync(zipFilePath);
18+
19+
return entries;
20+
};
21+
22+
const baseName = (filePath: string): string => {
23+
return filePath.split('/').pop() || filePath;
1624
};
1725

18-
export {downloadZipAndUnzip};
26+
export {downloadZipAndUnzip, baseName};

0 commit comments

Comments
 (0)