mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
better i18n checks, better action disabled, fix cert download for ios
This commit is contained in:
@@ -6,4 +6,5 @@ module.exports = {
|
||||
'projects/marketplace/**/*.ts': () => 'npm run check:marketplace',
|
||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
||||
'projects/start-tunnel/**/*.ts': () => 'npm run check:tunnel',
|
||||
'projects/**/*.{ts,html}': () => 'npm run check:i18n',
|
||||
}
|
||||
|
||||
@@ -6,17 +6,18 @@
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:setup",
|
||||
"check": "npm run check:i18n && npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:setup",
|
||||
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:tunnel": "tsc --project projects/start-tunnel/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:i18n": "node scripts/check-i18n.mjs",
|
||||
"build:deps": "rimraf .angular/cache && (cd ../sdk && make bundle) && (cd ../patch-db/client && npm ci && npm run build)",
|
||||
"build:setup": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build",
|
||||
"build:ui:dev": "ng run ui:build:development",
|
||||
"build:tunnel": "ng run start-tunnel:build",
|
||||
"build:setup": "npm run check:i18n && ng run setup-wizard:build",
|
||||
"build:ui": "npm run check:i18n && ng run ui:build",
|
||||
"build:ui:dev": "npm run check:i18n && ng run ui:build:development",
|
||||
"build:tunnel": "npm run check:i18n && ng run start-tunnel:build",
|
||||
"build:all": "npm run build:deps && npm run build:setup && npm run build:ui",
|
||||
"build:shared": "ng build shared",
|
||||
"build:marketplace": "npm run build:shared && ng build marketplace",
|
||||
|
||||
@@ -215,7 +215,7 @@ export default class SuccessPage implements AfterViewInit {
|
||||
.getElementById('cert')
|
||||
?.setAttribute(
|
||||
'href',
|
||||
`data:application/x-x509-ca-cert;base64,${this.result!.rootCa}`,
|
||||
`data:application/octet-stream;base64,${this.result!.rootCa}`,
|
||||
)
|
||||
|
||||
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// prettier-ignore
|
||||
export const ENGLISH = {
|
||||
export const ENGLISH: Record<string, number> = {
|
||||
'Change': 1, // verb
|
||||
'Update': 2, // verb
|
||||
'System': 4, // as in, system preferences
|
||||
@@ -680,4 +680,4 @@ export const ENGLISH = {
|
||||
'Installation Complete!': 714,
|
||||
'StartOS has been installed successfully.': 715,
|
||||
'Continue to Setup': 716,
|
||||
} as const
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export class i18nPipe implements PipeTransform {
|
||||
transform(englishKey: i18nKey | null | undefined | ''): string {
|
||||
englishKey = englishKey || ('' as i18nKey)
|
||||
|
||||
return this.i18n()?.[ENGLISH[englishKey]] || englishKey
|
||||
const id = ENGLISH[englishKey]
|
||||
|
||||
return (id !== undefined && this.i18n()?.[id]) || englishKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ const INACTIVE: PrimaryStatus[] = [
|
||||
'backing-up',
|
||||
]
|
||||
|
||||
const ALLOWED_STATUSES: Record<T.AllowedStatuses, Set<string>> = {
|
||||
'only-running': new Set(['running']),
|
||||
'only-stopped': new Set(['stopped']),
|
||||
any: new Set([
|
||||
'running',
|
||||
'stopped',
|
||||
'restarting',
|
||||
'restoring',
|
||||
'stopping',
|
||||
'starting',
|
||||
'backing-up',
|
||||
'task-required',
|
||||
]),
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
@if (package(); as pkg) {
|
||||
@@ -92,8 +107,9 @@ export default class ServiceActionsRoute {
|
||||
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
||||
? 'Other'
|
||||
: 'General'
|
||||
const status = renderPkgStatus(pkg).primary
|
||||
return {
|
||||
status: renderPkgStatus(pkg).primary,
|
||||
status,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.entries(pkg.actions)
|
||||
@@ -102,6 +118,13 @@ export default class ServiceActionsRoute {
|
||||
...action,
|
||||
id,
|
||||
group: action.group || specialGroup,
|
||||
visibility: ALLOWED_STATUSES[action.allowedStatuses].has(
|
||||
status,
|
||||
)
|
||||
? action.visibility
|
||||
: ({
|
||||
disabled: `${this.i18n.transform('Action can only be executed when service is')} ${this.i18n.transform(action.allowedStatuses === 'only-running' ? 'Running' : 'Stopped')?.toLowerCase()}`,
|
||||
} as T.ActionVisibility),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.group === specialGroup && b.group !== specialGroup)
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
DialogService,
|
||||
ErrorService,
|
||||
i18nKey,
|
||||
i18nPipe,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
@@ -16,21 +15,6 @@ import { ActionSuccessPage } from 'src/app/routes/portal/routes/services/modals/
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
|
||||
const allowedStatuses = {
|
||||
'only-running': new Set(['running']),
|
||||
'only-stopped': new Set(['stopped']),
|
||||
any: new Set([
|
||||
'running',
|
||||
'stopped',
|
||||
'restarting',
|
||||
'restoring',
|
||||
'stopping',
|
||||
'starting',
|
||||
'backing-up',
|
||||
'task-required',
|
||||
]),
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -40,14 +24,10 @@ export class ActionService {
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
private readonly i18n = inject(i18nPipe)
|
||||
|
||||
async present(data: PackageActionData) {
|
||||
const { pkgInfo, actionInfo } = data
|
||||
|
||||
if (
|
||||
allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status)
|
||||
) {
|
||||
if (actionInfo.metadata.hasInput) {
|
||||
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
||||
label: actionInfo.metadata.name as i18nKey,
|
||||
@@ -71,28 +51,6 @@ export class ActionService {
|
||||
this.execute(pkgInfo.id, null, actionInfo.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]]
|
||||
const last = statuses.pop()
|
||||
let statusesStr = statuses.join(', ')
|
||||
if (statuses.length) {
|
||||
if (statuses.length > 1) {
|
||||
// oxford comma
|
||||
statusesStr += ','
|
||||
}
|
||||
statusesStr += ` or ${last}`
|
||||
} else if (last) {
|
||||
statusesStr = last
|
||||
}
|
||||
|
||||
this.dialog
|
||||
.openAlert(
|
||||
`${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey,
|
||||
{ label: 'Forbidden' },
|
||||
)
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async execute(
|
||||
|
||||
95
web/scripts/check-i18n.mjs
Normal file
95
web/scripts/check-i18n.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import { readFileSync, readdirSync, statSync } from 'fs'
|
||||
import { join, relative } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
const root = join(__dirname, '..')
|
||||
|
||||
// Extract dictionary keys from en.ts
|
||||
const enPath = join(
|
||||
root,
|
||||
'projects/shared/src/i18n/dictionaries/en.ts',
|
||||
)
|
||||
const enSource = readFileSync(enPath, 'utf-8')
|
||||
const validKeys = new Set()
|
||||
|
||||
for (const match of enSource.matchAll(/^\s+'(.+?)':\s*\d+/gm)) {
|
||||
validKeys.add(match[1])
|
||||
}
|
||||
|
||||
if (validKeys.size === 0) {
|
||||
console.error('ERROR: Could not parse any keys from en.ts')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Loaded ${validKeys.size} i18n keys from en.ts`)
|
||||
|
||||
// Collect all .ts and .html files under projects/
|
||||
function walk(dir, files = []) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = join(dir, entry)
|
||||
if (entry === 'node_modules' || entry === 'dist') continue
|
||||
const stat = statSync(full)
|
||||
if (stat.isDirectory()) {
|
||||
walk(full, files)
|
||||
} else if (full.endsWith('.ts') || full.endsWith('.html')) {
|
||||
files.push(full)
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
const projectsDir = join(root, 'projects')
|
||||
const files = walk(projectsDir)
|
||||
|
||||
const errors = []
|
||||
|
||||
for (const file of files) {
|
||||
// Skip the dictionary files themselves
|
||||
if (file.includes('/i18n/dictionaries/')) continue
|
||||
|
||||
const source = readFileSync(file, 'utf-8')
|
||||
const lines = source.split('\n')
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
// Pattern 1: i18n.transform('Key') or i18n.transform("Key")
|
||||
for (const m of line.matchAll(/i18n\.transform\(\s*'([^']+)'\s*\)/g)) {
|
||||
if (!validKeys.has(m[1])) {
|
||||
errors.push({ file, line: i + 1, key: m[1] })
|
||||
}
|
||||
}
|
||||
for (const m of line.matchAll(/i18n\.transform\(\s*"([^"]+)"\s*\)/g)) {
|
||||
if (!validKeys.has(m[1])) {
|
||||
errors.push({ file, line: i + 1, key: m[1] })
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: 'Key' | i18n or "Key" | i18n (Angular templates)
|
||||
for (const m of line.matchAll(/'([^']+)'\s*\|\s*i18n/g)) {
|
||||
if (!validKeys.has(m[1])) {
|
||||
errors.push({ file, line: i + 1, key: m[1] })
|
||||
}
|
||||
}
|
||||
for (const m of line.matchAll(/"([^"]+)"\s*\|\s*i18n/g)) {
|
||||
if (!validKeys.has(m[1])) {
|
||||
errors.push({ file, line: i + 1, key: m[1] })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`\nFound ${errors.length} invalid i18n key(s):\n`)
|
||||
for (const { file, line, key } of errors) {
|
||||
const rel = relative(root, file)
|
||||
console.error(` ${rel}:${line} "${key}"`)
|
||||
}
|
||||
console.error()
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log('All i18n keys are valid.')
|
||||
}
|
||||
Reference in New Issue
Block a user