Move away from smartling CLI

This commit is contained in:
Fedor Indutny 2024-03-21 11:31:31 -07:00 committed by GitHub
parent e90553b3b3
commit f55e6e3407
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 121916 additions and 244506 deletions

View file

@ -0,0 +1,44 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { readdir, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
async function main(): Promise<void> {
const rootDir = join(__dirname, '..', '..');
const sourceDir = join(rootDir, '_locales');
const targetDir = join(rootDir, 'build', 'compact-locales');
const locales = await readdir(sourceDir);
await Promise.all(
locales.map(async locale => {
const sourcePath = join(sourceDir, locale, 'messages.json');
const targetPath = join(targetDir, locale, 'messages.json');
await mkdir(dirname(targetPath), { recursive: true });
const json = JSON.parse(await readFile(sourcePath, 'utf8'));
for (const value of Object.values(json)) {
const typedValue = value as { description?: string };
delete typedValue.description;
}
delete json.smartling;
const entries = [...Object.entries(json)];
// Sort entries alphabetically for better incremental updates.
entries.sort(([a], [b]) => {
return a < b ? -1 : 1;
});
const result = Object.fromEntries(entries);
await writeFile(targetPath, JSON.stringify(result));
})
);
}
main().catch(err => {
console.error(err);
process.exit(1);
});

View file

@ -1,72 +1,120 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { execSync } from 'child_process';
import fsExtra from 'fs-extra';
import path from 'path';
import { rm, mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { Readable } from 'node:stream';
import fastGlob from 'fast-glob';
import unzipper from 'unzipper';
import prettier from 'prettier';
import { authenticate, API_BASE, PROJECT_ID } from '../util/smartling';
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
if (!SMARTLING_USER) {
console.error('Need to set SMARTLING_USER environment variable!');
process.exit(1);
}
if (!SMARTLING_SECRET) {
console.error('Need to set SMARTLING_SECRET environment variable!');
process.exit(1);
}
const RENAMES = new Map([
// Smartling uses "zh-YU" for Cantonese (or Yue Chinese).
// This is wrong.
// The language tag for Yue Chinese is "yue"
// "zh-YU" actually implies "Chinese as spoken in Yugoslavia (canonicalized to Serbia)"
['zh-YU', 'yue'],
console.log('Cleaning _locales directory...');
const dirEntries = fastGlob.sync(['_locales/*', '!_locales/en'], {
onlyDirectories: true,
absolute: true,
});
// For most of the Chinese-speaking world, where we don't have a region specific
// locale available (e.g. zh-HK), zh-TW is a suitable choice for "Traditional Chinese".
//
// However, Intl.LocaleMatcher won't match "zh-Hant-XX" to "zh-TW",
// we need to rename it to "zh-Hant" explicitly to make it work.
['zh-TW', 'zh-Hant'],
for (const dirEntry of dirEntries) {
fsExtra.rmdirSync(dirEntry, { recursive: true });
}
// "YR" is not a valid region subtag. Smartling made it up.
['sr-YR', 'sr'],
]);
console.log('Fetching latest strings!');
console.log();
execSync(
'smartling-cli' +
` --user "${SMARTLING_USER}"` +
` --secret "${SMARTLING_SECRET}"` +
' --config .smartling.yml' +
' --verbose' +
' --format "_locales/{{.Locale}}/messages.json"' +
' files pull',
{
stdio: [null, process.stdout, process.stderr],
async function main() {
if (!SMARTLING_USER) {
console.error('Need to set SMARTLING_USER environment variable!');
process.exit(1);
}
if (!SMARTLING_SECRET) {
console.error('Need to set SMARTLING_SECRET environment variable!');
process.exit(1);
}
);
function rename(from: string, to: string) {
console.log(`Renaming "${from}" to "${to}"`);
fsExtra.moveSync(path.join('_locales', from), path.join('_locales', to), {
overwrite: true,
console.log('Authenticating with Smartling');
const headers = await authenticate({
userIdentifier: SMARTLING_USER,
userSecret: SMARTLING_SECRET,
});
const zipURL = new URL(
`./files-api/v2/projects/${PROJECT_ID}/locales/all/file/zip`,
API_BASE
);
zipURL.searchParams.set('fileUri', '_locales/en/messages.json');
zipURL.searchParams.set('retrievalType', 'published');
zipURL.searchParams.set('includeOriginalStrings', 'true');
const fileRes = await fetch(zipURL, {
headers,
});
if (!fileRes.ok) {
throw new Error('Failed to fetch the file');
}
if (!fileRes.body) {
throw new Error('Missing body');
}
console.log('Cleaning _locales directory...');
const dirEntries = await fastGlob(['_locales/*', '!_locales/en'], {
onlyDirectories: true,
absolute: true,
});
await Promise.all(
dirEntries.map(dirEntry => rm(dirEntry, { recursive: true }))
);
console.log('Getting latest strings');
const prettierConfig = await prettier.resolveConfig('_locales');
const zip = Readable.from(
fileRes.body as unknown as AsyncIterable<Uint8Array>
).pipe(unzipper.Parse({ forceStream: true }));
for await (const entry of zip) {
if (entry.type !== 'File') {
entry.autodrain();
continue;
}
let [locale] = entry.path.split(/[\\/]/, 1);
locale = RENAMES.get(locale) ?? locale;
const targetDir = path.join('_locales', locale);
try {
await mkdir(targetDir);
} catch (error) {
console.error(error);
}
const targetFile = path.join(targetDir, 'messages.json');
console.log('Writing', locale);
const json = JSON.parse((await entry.buffer()).toString());
for (const value of Object.values(json)) {
const typedValue = value as { description?: string };
delete typedValue.description;
}
delete json.smartling;
const output = prettier.format(JSON.stringify(json, null, 2), {
...prettierConfig,
filepath: targetFile,
});
await writeFile(targetFile, output);
}
}
// Smartling uses "zh-YU" for Cantonese (or Yue Chinese).
// This is wrong.
// The language tag for Yue Chinese is "yue"
// "zh-YU" actually implies "Chinese as spoken in Yugoslavia (canonicalized to Serbia)"
rename('zh-YU', 'yue');
// For most of the Chinese-speaking world, where we don't have a region specific
// locale available (e.g. zh-HK), zh-TW is a suitable choice for "Traditional Chinese".
//
// However, Intl.LocaleMatcher won't match "zh-Hant-XX" to "zh-TW",
// we need to rename it to "zh-Hant" explicitly to make it work.
rename('zh-TW', 'zh-Hant');
// "YR" is not a valid region subtag. Smartling made it up.
rename('sr-YR', 'sr');
console.log('Formatting newly-downloaded strings!');
console.log();
execSync('yarn format', {
stdio: [null, process.stdout, process.stderr],
main().catch(err => {
console.error(err);
process.exit(1);
});

View file

@ -1,29 +1,71 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { execSync } from 'child_process';
import { randomBytes } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { API_BASE, PROJECT_ID, authenticate } from '../util/smartling';
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
if (!SMARTLING_USER) {
console.error('Need to set SMARTLING_USER environment variable!');
process.exit(1);
}
if (!SMARTLING_SECRET) {
console.error('Need to set SMARTLING_SECRET environment variable!');
process.exit(1);
async function main() {
if (!SMARTLING_USER) {
console.error('Need to set SMARTLING_USER environment variable!');
process.exit(1);
}
if (!SMARTLING_SECRET) {
console.error('Need to set SMARTLING_SECRET environment variable!');
process.exit(1);
}
console.log('Authenticating with Smartling');
const headers = await authenticate({
userIdentifier: SMARTLING_USER,
userSecret: SMARTLING_SECRET,
});
const boundaryString = randomBytes(32).toString('hex');
headers.set(
'content-type',
`multipart/form-data; boundary=${boundaryString}`
);
const url = new URL(`./files-api/v2/projects/${PROJECT_ID}/file`, API_BASE);
const body = [
`--${boundaryString}`,
'Content-Disposition: form-data; name="fileUri"',
'Content-Type: text/plain',
'',
'_locales/en/messages.json',
`--${boundaryString}`,
'Content-Disposition: form-data; name="fileType"',
'Content-Type: text/plain',
'',
'json',
`--${boundaryString}`,
'Content-Disposition: form-data; name="file"',
'Content-Type: text/plain',
'',
await readFile('_locales/en/messages.json', 'utf8'),
`--${boundaryString}`,
'',
];
console.log('Pushing strings');
const res = await fetch(url, {
method: 'POST',
headers,
body: body.join('\r\n'),
});
if (!res.ok) {
throw new Error(`Failed to push strings: ${await res.text()}`);
}
}
console.log('Pushing latest strings!');
console.log();
execSync(
'smartling-cli' +
` --user "${SMARTLING_USER}"` +
` --secret "${SMARTLING_SECRET}"` +
' --config .smartling.yml' +
' --verbose' +
' files push _locales/en/messages.json',
{
stdio: [null, process.stdout, process.stderr],
}
);
main().catch(err => {
console.error(err);
process.exit(1);
});