signal-desktop/ts/scripts/build-localized-display-names.ts

138 lines
3.7 KiB
TypeScript
Raw Normal View History

// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { parse } from 'csv-parse';
import fs from 'fs/promises';
import { z } from 'zod';
import { _getAvailableLocales } from '../../app/locale';
2024-10-02 19:03:10 +00:00
import { parseUnknown } from '../util/schemas';
2024-02-15 02:08:21 +00:00
const type = process.argv[2];
if (type !== 'countries' && type !== 'locales') {
throw new Error('Invalid first argument, expceted "countries" or "locales"');
}
const localeDisplayNamesDataPath = process.argv[3];
if (!localeDisplayNamesDataPath) {
throw new Error('Missing second argument: source csv file');
}
const localeDisplayNamesBuildPath = process.argv[4];
if (!localeDisplayNamesBuildPath) {
throw new Error('Missing third argument: output json file');
}
const availableLocales = _getAvailableLocales();
const LocaleString = z.string().refine(arg => {
try {
return new Intl.Locale(arg) && true;
} catch {
return false;
}
});
const LocaleDisplayNames = z
2024-02-15 02:08:21 +00:00
.tuple([
z
.tuple([z.literal(type === 'locales' ? 'locale' : 'Country Code')])
.rest(LocaleString),
])
.rest(z.tuple([LocaleString]).rest(z.string()));
type Row = ReadonlyArray<string>;
type Records = ReadonlyArray<Row>;
function parseCsv(input: string) {
return new Promise<Records>((resolve, reject) => {
2023-11-14 21:08:28 +00:00
parse(input, { trim: true }, (error, records: Records) => {
if (error) {
reject(error);
} else {
resolve(records);
}
});
});
}
type LocaleDisplayNamesResult = Record<string, Record<string, string>>;
function convertData(
input: z.infer<typeof LocaleDisplayNames>
): LocaleDisplayNamesResult {
2023-11-14 21:08:28 +00:00
const [[, ...keys], ...rows] = input;
const result: LocaleDisplayNamesResult = {};
if (type === 'locales') {
for (const row of rows) {
const [subKey, ...messages] = row;
result[subKey] = {};
for (const [index, message] of messages.entries()) {
result[subKey][keys[index]] = message;
}
}
} else {
// Countries use transposed matrix.
for (const key of keys) {
result[key] = {};
}
for (const row of rows) {
const [subKey, ...messages] = row;
for (const [index, message] of messages.entries()) {
result[keys[index]][subKey] = message;
}
}
}
return result;
}
function assertValuesForAllLocales(result: LocaleDisplayNamesResult) {
for (const locale of availableLocales) {
const values = result[locale];
if (values == null) {
throw new Error(`Missing values for locale ${locale}`);
}
for (const innerLocale of availableLocales) {
if (values[innerLocale] == null) {
throw new Error(`Missing value for locale ${locale} -> ${innerLocale}`);
}
}
}
}
function assertValuesForAllCountries(result: LocaleDisplayNamesResult) {
const availableCountries = Object.keys(result.en);
for (const locale of availableLocales) {
const values = result[locale];
if (values == null) {
throw new Error(`Missing values for locale ${locale}`);
}
for (const country of availableCountries) {
if (values[country] == null) {
throw new Error(`Missing value for country ${locale} -> ${country}`);
}
}
}
}
async function main() {
const contents = await fs.readFile(localeDisplayNamesDataPath, 'utf-8');
const records = await parseCsv(contents);
2024-10-02 19:03:10 +00:00
const data = parseUnknown(LocaleDisplayNames, records as unknown);
const result = convertData(data);
if (type === 'locales') {
assertValuesForAllLocales(result);
} else if (type === 'countries') {
assertValuesForAllCountries(result);
}
2023-11-14 21:08:28 +00:00
const json = JSON.stringify(result, null, 2);
await fs.writeFile(localeDisplayNamesBuildPath, json, 'utf-8');
}
main().catch(error => {
console.error(error);
process.exit(1);
});