Merge pull request #21290 from overleaf/ls-scripts-to-esm-translations

Migrate scripts/translation to esm

GitOrigin-RevId: 475ec949f0ba238791df91de109169584e68c701
This commit is contained in:
Liangjun Song 2024-10-24 13:18:37 +01:00 committed by Copybot
parent 52edad6f12
commit 8293771f58
15 changed files with 195 additions and 134 deletions

View file

@ -1,21 +1,25 @@
const fs = require('fs') import fs from 'fs'
const Path = require('path') import Path from 'path'
import { fileURLToPath } from 'node:url'
import { loadLocale } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const LOCALES = Path.join(__dirname, '../../locales') const LOCALES = Path.join(__dirname, '../../locales')
const SORT_BY_PROGRESS = process.argv.includes('--sort-by-progress') const SORT_BY_PROGRESS = process.argv.includes('--sort-by-progress')
function count(file) { function count(language) {
return Object.keys(require(Path.join(LOCALES, file))).length const locale = loadLocale(language)
return Object.keys(locale).length
} }
async function main() { async function main() {
const EN = count('en.json') const EN = count('en')
const rows = [] const rows = []
for (const file of await fs.promises.readdir(LOCALES)) { for (const file of await fs.promises.readdir(LOCALES)) {
if (file === 'README.md') continue if (file === 'README.md') continue
const n = count(file)
const name = file.replace('.json', '') const name = file.replace('.json', '')
const n = count(name)
rows.push({ rows.push({
name, name,
done: n, done: n,
@ -29,7 +33,9 @@ async function main() {
console.table(rows) console.table(rows)
} }
main().catch(error => { try {
await main()
} catch (error) {
console.error(error) console.error(error)
process.exit(1) process.exit(1)
}) }

View file

@ -1,17 +1,19 @@
const Path = require('path') import Path from 'path'
const fs = require('fs') import fs from 'fs'
const { sanitize } = require('./sanitize') import Senitize from './sanitize.js'
import { fileURLToPath } from 'node:url'
import { loadLocale } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const { sanitize } = Senitize
async function main() { async function main() {
let ok = true let ok = true
const base = Path.join(__dirname, '/../../locales') const base = Path.join(__dirname, '/../../locales')
for (const name of await fs.promises.readdir(base)) { for (const name of await fs.promises.readdir(base)) {
if (name === 'README.md') continue if (name === 'README.md') continue
const blob = await fs.promises.readFile( const language = name.replace('.json', '')
Path.join(__dirname, '/../../locales', name), const locales = loadLocale(language)
'utf-8'
)
const locales = JSON.parse(blob)
for (const key of Object.keys(locales)) { for (const key of Object.keys(locales)) {
const want = locales[key] const want = locales[key]
@ -32,11 +34,10 @@ async function main() {
} }
} }
main() try {
.then(() => { await main()
process.exit(0) process.exit(0)
}) } catch (error) {
.catch(error => { console.error(error)
console.error(error) process.exit(1)
process.exit(1) }
})

View file

@ -1,10 +1,13 @@
const fs = require('fs') import fs from 'fs'
const Path = require('path') import Path from 'path'
import { fileURLToPath } from 'node:url'
import { loadLocale } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const GLOBALS = ['__appName__'] const GLOBALS = ['__appName__']
const LOCALES = Path.join(__dirname, '../../locales') const LOCALES = Path.join(__dirname, '../../locales')
const baseLocalePath = Path.join(LOCALES, 'en.json') const baseLocale = loadLocale('en')
const baseLocale = JSON.parse(fs.readFileSync(baseLocalePath, 'utf-8'))
const baseLocaleKeys = Object.keys(baseLocale) const baseLocaleKeys = Object.keys(baseLocale)
const IGNORE_ORPHANED_TRANSLATIONS = process.argv.includes( const IGNORE_ORPHANED_TRANSLATIONS = process.argv.includes(
@ -41,9 +44,7 @@ function difference(key, base, target) {
let violations = 0 let violations = 0
for (const localeName of fs.readdirSync(LOCALES)) { for (const localeName of fs.readdirSync(LOCALES)) {
if (localeName === 'README.md') continue if (localeName === 'README.md') continue
const localePath = Path.join(LOCALES, localeName) const locale = loadLocale(localeName.replace('.json', ''))
const locale = JSON.parse(fs.readFileSync(localePath, 'utf-8'))
for (const key of Object.keys(locale)) { for (const key of Object.keys(locale)) {
if (!baseLocaleKeys.includes(key)) { if (!baseLocaleKeys.includes(key)) {

View file

@ -1,7 +1,10 @@
const fs = require('fs') import fs from 'fs'
const Path = require('path') import Path from 'path'
const { execSync } = require('child_process') import { execSync } from 'child_process'
import { fileURLToPath } from 'node:url'
import { loadLocale } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const EN_JSON = Path.join(__dirname, '../../locales/en.json') const EN_JSON = Path.join(__dirname, '../../locales/en.json')
const CHECK = process.argv.includes('--check') const CHECK = process.argv.includes('--check')
const SYNC_NON_EN = process.argv.includes('--sync-non-en') const SYNC_NON_EN = process.argv.includes('--sync-non-en')
@ -17,7 +20,7 @@ const COUNT_SUFFIXES = [
] ]
async function main() { async function main() {
const locales = JSON.parse(await fs.promises.readFile(EN_JSON, 'utf-8')) const locales = loadLocale('en')
const src = execSync( const src = execSync(
// - find all the app source files in web // - find all the app source files in web
@ -112,7 +115,7 @@ async function main() {
if (name === 'README.md') continue if (name === 'README.md') continue
if (name === 'en.json') continue if (name === 'en.json') continue
const path = Path.join(LOCALES, name) const path = Path.join(LOCALES, name)
const locales = JSON.parse(await fs.promises.readFile(path, 'utf-8')) const locales = loadLocale(name.replace('.json', ''))
for (const key of Object.keys(locales)) { for (const key of Object.keys(locales)) {
if (!found.has(key)) { if (!found.has(key)) {
delete locales[key] delete locales[key]
@ -154,7 +157,9 @@ async function main() {
await fs.promises.writeFile(EN_JSON, sorted) await fs.promises.writeFile(EN_JSON, sorted)
} }
main().catch(error => { try {
await main()
} catch (error) {
console.error(error) console.error(error)
process.exit(1) process.exit(1)
}) }

View file

@ -1,7 +1,13 @@
import fs from 'fs'
import Path from 'path'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const ONESKY_SETTING_PATH = Path.join(__dirname, '../../data/onesky.json')
let userOptions let userOptions
try { try {
userOptions = require('../../data/onesky.json') userOptions = JSON.parse(fs.readFileSync(ONESKY_SETTING_PATH))
} catch (err) { } catch (err) {
if (err.code !== 'ENOENT') throw err
if (!process.env.ONE_SKY_PUBLIC_KEY) { if (!process.env.ONE_SKY_PUBLIC_KEY) {
console.error( console.error(
'Cannot detect onesky credentials.\n\tDevelopers: see the docs at', 'Cannot detect onesky credentials.\n\tDevelopers: see the docs at',
@ -24,6 +30,6 @@ function withAuth(options) {
) )
} }
module.exports = { export default {
withAuth, withAuth,
} }

View file

@ -1,52 +1,59 @@
const path = require('path') import path from 'path'
const { promises: fs } = require('fs') import { promises as fs } from 'fs'
const oneSky = require('@brainly/onesky-utils') import oneSky from '@brainly/onesky-utils'
const { sanitize } = require('./sanitize') import Sanitize from './sanitize.js'
const { withAuth } = require('./config') import Config from './config.js'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const { sanitize } = Sanitize
const { withAuth } = Config
async function run() { async function run() {
try { // The recommended OneSky set-up appears to require an API request to
// The recommended OneSky set-up appears to require an API request to // generate files on their side, which you could then request and use. We
// generate files on their side, which you could then request and use. We // only have 1 such file that appears to be misnamed (en-US, despite our
// only have 1 such file that appears to be misnamed (en-US, despite our // translations being marked as GB) and very out-of-date.
// translations being marked as GB) and very out-of-date. // However by requesting the "multilingual file" for this file, we get all
// However by requesting the "multilingual file" for this file, we get all // of the translations
// of the translations const content = await oneSky.getMultilingualFile(
const content = await oneSky.getMultilingualFile( withAuth({
withAuth({ fileName: 'en-US.json',
fileName: 'en-US.json', })
}) )
) const json = JSON.parse(content)
const json = JSON.parse(content)
for (const [code, lang] of Object.entries(json)) { for (const [code, lang] of Object.entries(json)) {
if (code === 'en-GB') { if (code === 'en-GB') {
// OneSky does not have read-after-write consistency. // OneSky does not have read-after-write consistency.
// Skip the dump of English locales, which may not include locales // Skip the dump of English locales, which may not include locales
// that were just uploaded. // that were just uploaded.
continue continue
}
for (let [key, value] of Object.entries(lang.translation)) {
// Handle multi-line strings as arrays by joining on newline
if (Array.isArray(value)) {
value = value.join('\n')
}
lang.translation[key] = sanitize(value)
}
await fs.writeFile(
path.join(__dirname, `/../../locales/${code}.json`),
JSON.stringify(
lang.translation,
Object.keys(lang.translation).sort(),
2
) + '\n'
)
} }
} catch (error) {
console.error(error) for (let [key, value] of Object.entries(lang.translation)) {
process.exit(1) // Handle multi-line strings as arrays by joining on newline
if (Array.isArray(value)) {
value = value.join('\n')
}
lang.translation[key] = sanitize(value)
}
await fs.writeFile(
path.join(__dirname, `/../../locales/${code}.json`),
JSON.stringify(
lang.translation,
Object.keys(lang.translation).sort(),
2
) + '\n'
)
} }
} }
run()
try {
await run()
} catch (error) {
console.error(error)
process.exit(1)
}

View file

@ -13,6 +13,9 @@
localeKey: ['key1', 'key2'] localeKey: ['key1', 'key2']
click_here_to_view_sl_in_lng: ['lngName'] click_here_to_view_sl_in_lng: ['lngName']
*/ */
import TransformLocales from './transformLocales.js'
import { fileURLToPath } from 'url'
const MAPPING = { const MAPPING = {
support_lots_of_features: ['help_guides_link'], support_lots_of_features: ['help_guides_link'],
nothing_to_install_ready_to_go: ['start_now'], nothing_to_install_ready_to_go: ['start_now'],
@ -37,8 +40,6 @@ const MAPPING = {
click_here_to_view_sl_in_lng: ['lngName'], click_here_to_view_sl_in_lng: ['lngName'],
} }
const { transformLocales } = require('./transformLocales')
function transformLocale(locale, components) { function transformLocale(locale, components) {
components.forEach((key, idx) => { components.forEach((key, idx) => {
const i18nKey = `__${key}__` const i18nKey = `__${key}__`
@ -51,9 +52,12 @@ function transformLocale(locale, components) {
} }
function main() { function main() {
transformLocales(MAPPING, transformLocale) TransformLocales.transformLocales(MAPPING, transformLocale)
} }
if (require.main === module) { if (
fileURLToPath(import.meta.url).replace(/\.js$/, '') ===
process.argv[1].replace(/\.js$/, '')
) {
main() main()
} }

View file

@ -4,5 +4,6 @@
"node-fetch": "^2.7.0", "node-fetch": "^2.7.0",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
"yargs": "^17.7.2" "yargs": "^17.7.2"
} },
"type": "module"
} }

View file

@ -13,13 +13,14 @@
localeKey: ['keyLinkOpen', 'keyLinkClose'] localeKey: ['keyLinkOpen', 'keyLinkClose']
faq_pay_by_invoice_answer: ['payByInvoiceLinkOpen', 'payByInvoiceLinkClose'] faq_pay_by_invoice_answer: ['payByInvoiceLinkOpen', 'payByInvoiceLinkClose']
*/ */
import TransformLocales from './transformLocales.js'
import { fileURLToPath } from 'url'
const MAPPING = { const MAPPING = {
also_provides_free_plan: ['registerLinkOpen', 'registerLinkClose'], also_provides_free_plan: ['registerLinkOpen', 'registerLinkClose'],
faq_pay_by_invoice_answer: ['payByInvoiceLinkOpen', 'payByInvoiceLinkClose'], faq_pay_by_invoice_answer: ['payByInvoiceLinkOpen', 'payByInvoiceLinkClose'],
} }
const { transformLocales } = require('./transformLocales')
function transformLocale(locale, [open, close]) { function transformLocale(locale, [open, close]) {
const i18nOpen = `__${open}__` const i18nOpen = `__${open}__`
const i18nClose = `__${close}__` const i18nClose = `__${close}__`
@ -30,9 +31,12 @@ function transformLocale(locale, [open, close]) {
} }
function main() { function main() {
transformLocales(MAPPING, transformLocale) TransformLocales.transformLocales(MAPPING, transformLocale)
} }
if (require.main === module) { if (
fileURLToPath(import.meta.url).replace(/\.js$/, '') ===
process.argv[1].replace(/\.js$/, '')
) {
main() main()
} }

View file

@ -1,4 +1,4 @@
const sanitizeHtml = require('sanitize-html') import sanitizeHtml from 'sanitize-html'
/** /**
* Sanitize a translation string to prevent injection attacks * Sanitize a translation string to prevent injection attacks
@ -43,4 +43,4 @@ function sanitize(input) {
) )
} }
module.exports = { sanitize } export default { sanitize }

View file

@ -1,5 +1,8 @@
const fs = require('fs') import fs from 'fs'
const Path = require('path') import Path from 'path'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const LOCALES = Path.join(__dirname, '../../locales') const LOCALES = Path.join(__dirname, '../../locales')
const CHECK = process.argv.includes('--check') const CHECK = process.argv.includes('--check')
@ -30,7 +33,9 @@ async function main() {
} }
} }
main().catch(error => { try {
await main()
} catch (error) {
console.error(error) console.error(error)
process.exit(1) process.exit(1)
}) }

View file

@ -1,5 +1,9 @@
const Path = require('path') import Path from 'path'
const fs = require('fs') import fs from 'fs'
import { fileURLToPath } from 'node:url'
import { loadLocale } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const LANGUAGES = [ const LANGUAGES = [
'cs', 'cs',
@ -24,7 +28,7 @@ const LANGUAGES = [
const LOCALES = {} const LOCALES = {}
LANGUAGES.forEach(loadLocales) LANGUAGES.forEach(loadLocales)
function loadLocales(language) { function loadLocales(language) {
LOCALES[language] = require(`../../locales/${language}.json`) LOCALES[language] = loadLocale(language)
} }
function transformLocales(mapping, transformLocale) { function transformLocales(mapping, transformLocale) {
@ -45,6 +49,6 @@ function transformLocales(mapping, transformLocale) {
}) })
} }
module.exports = { export default {
transformLocales, transformLocales,
} }

View file

@ -1,14 +1,19 @@
const yargs = require('yargs/yargs') import yargs from 'yargs/yargs'
const { hideBin } = require('yargs/helpers') import { hideBin } from 'yargs/helpers'
const Path = require('path') import Path from 'path'
const fs = require('fs') import fs from 'fs'
import { fileURLToPath } from 'url'
import Settings from '../../config/settings.defaults.js'
import Readline from 'readline'
import { loadLocale as loadTranslations } from './utils.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const LOCALES = Path.join(__dirname, '../../locales') const LOCALES = Path.join(__dirname, '../../locales')
const VALID_LOCALES = Object.keys( const VALID_LOCALES = Object.keys(Settings.translatedLanguages).filter(
require('../../config/settings.defaults').translatedLanguages locale => locale !== 'en'
).filter(locale => locale !== 'en') )
const readline = require('readline').createInterface({ const readline = Readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
}) })
@ -91,12 +96,6 @@ async function translateLocales() {
} }
} }
async function loadTranslations(locale) {
return JSON.parse(
fs.readFileSync(Path.join(LOCALES, `${locale}.json`), 'utf-8')
)
}
function prompt(text) { function prompt(text) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
readline.question(text, value => { readline.question(text, value => {
@ -105,11 +104,10 @@ function prompt(text) {
) )
} }
translateLocales() try {
.then(() => { await translateLocales()
process.exit(0) process.exit(0)
}) } catch (error) {
.catch(error => { console.error(error)
console.error(error) process.exit(1)
process.exit(1) }
})

View file

@ -1,8 +1,13 @@
const Path = require('path') import Path from 'path'
const { promises: fs } = require('fs') import { promises as fs } from 'fs'
const { promisify } = require('util') import { promisify } from 'util'
const oneSky = require('@brainly/onesky-utils') import oneSky from '@brainly/onesky-utils'
const { withAuth } = require('./config') import Config from './config.js'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const { withAuth } = Config
const sleep = promisify(setTimeout) const sleep = promisify(setTimeout)
@ -58,7 +63,9 @@ async function main() {
} }
} }
main().catch(error => { try {
await main()
} catch (error) {
console.error({ error }) console.error({ error })
process.exit(1) process.exit(1)
}) }

View file

@ -0,0 +1,12 @@
import { fileURLToPath } from 'url'
import fs from 'fs'
import Path from 'path'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const LOCALES_FOLDER = Path.join(__dirname, '../../locales')
export function loadLocale(language) {
return JSON.parse(
fs.readFileSync(Path.join(LOCALES_FOLDER, `${language}.json`), 'utf-8')
)
}