mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +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/marketplace/**/*.ts': () => 'npm run check:marketplace',
|
||||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
||||||
'projects/start-tunnel/**/*.ts': () => 'npm run check:tunnel',
|
'projects/start-tunnel/**/*.ts': () => 'npm run check:tunnel',
|
||||||
|
'projects/**/*.{ts,html}': () => 'npm run check:i18n',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,18 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"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:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:marketplace": "tsc --project projects/marketplace/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:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||||
"check:ui": "tsc --project projects/ui/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: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: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:setup": "npm run check:i18n && ng run setup-wizard:build",
|
||||||
"build:ui": "ng run ui:build",
|
"build:ui": "npm run check:i18n && ng run ui:build",
|
||||||
"build:ui:dev": "ng run ui:build:development",
|
"build:ui:dev": "npm run check:i18n && ng run ui:build:development",
|
||||||
"build:tunnel": "ng run start-tunnel:build",
|
"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:all": "npm run build:deps && npm run build:setup && npm run build:ui",
|
||||||
"build:shared": "ng build shared",
|
"build:shared": "ng build shared",
|
||||||
"build:marketplace": "npm run build:shared && ng build marketplace",
|
"build:marketplace": "npm run build:shared && ng build marketplace",
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export default class SuccessPage implements AfterViewInit {
|
|||||||
.getElementById('cert')
|
.getElementById('cert')
|
||||||
?.setAttribute(
|
?.setAttribute(
|
||||||
'href',
|
'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 || ''
|
const html = this.documentation?.nativeElement.innerHTML || ''
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
export const ENGLISH = {
|
export const ENGLISH: Record<string, number> = {
|
||||||
'Change': 1, // verb
|
'Change': 1, // verb
|
||||||
'Update': 2, // verb
|
'Update': 2, // verb
|
||||||
'System': 4, // as in, system preferences
|
'System': 4, // as in, system preferences
|
||||||
@@ -680,4 +680,4 @@ export const ENGLISH = {
|
|||||||
'Installation Complete!': 714,
|
'Installation Complete!': 714,
|
||||||
'StartOS has been installed successfully.': 715,
|
'StartOS has been installed successfully.': 715,
|
||||||
'Continue to Setup': 716,
|
'Continue to Setup': 716,
|
||||||
} as const
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export class i18nPipe implements PipeTransform {
|
|||||||
transform(englishKey: i18nKey | null | undefined | ''): string {
|
transform(englishKey: i18nKey | null | undefined | ''): string {
|
||||||
englishKey = englishKey || ('' as i18nKey)
|
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',
|
'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({
|
@Component({
|
||||||
template: `
|
template: `
|
||||||
@if (package(); as pkg) {
|
@if (package(); as pkg) {
|
||||||
@@ -92,8 +107,9 @@ export default class ServiceActionsRoute {
|
|||||||
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
const specialGroup = Object.values(pkg.actions).some(a => !!a.group)
|
||||||
? 'Other'
|
? 'Other'
|
||||||
: 'General'
|
: 'General'
|
||||||
|
const status = renderPkgStatus(pkg).primary
|
||||||
return {
|
return {
|
||||||
status: renderPkgStatus(pkg).primary,
|
status,
|
||||||
icon: pkg.icon,
|
icon: pkg.icon,
|
||||||
manifest: getManifest(pkg),
|
manifest: getManifest(pkg),
|
||||||
actions: Object.entries(pkg.actions)
|
actions: Object.entries(pkg.actions)
|
||||||
@@ -102,6 +118,13 @@ export default class ServiceActionsRoute {
|
|||||||
...action,
|
...action,
|
||||||
id,
|
id,
|
||||||
group: action.group || specialGroup,
|
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) => {
|
.sort((a, b) => {
|
||||||
if (a.group === specialGroup && b.group !== specialGroup)
|
if (a.group === specialGroup && b.group !== specialGroup)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
DialogService,
|
DialogService,
|
||||||
ErrorService,
|
ErrorService,
|
||||||
i18nKey,
|
i18nKey,
|
||||||
i18nPipe,
|
|
||||||
LoadingService,
|
LoadingService,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
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 { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { FormDialogService } from 'src/app/services/form-dialog.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({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -40,58 +24,32 @@ export class ActionService {
|
|||||||
private readonly errorService = inject(ErrorService)
|
private readonly errorService = inject(ErrorService)
|
||||||
private readonly loader = inject(LoadingService)
|
private readonly loader = inject(LoadingService)
|
||||||
private readonly formDialog = inject(FormDialogService)
|
private readonly formDialog = inject(FormDialogService)
|
||||||
private readonly i18n = inject(i18nPipe)
|
|
||||||
|
|
||||||
async present(data: PackageActionData) {
|
async present(data: PackageActionData) {
|
||||||
const { pkgInfo, actionInfo } = data
|
const { pkgInfo, actionInfo } = data
|
||||||
|
|
||||||
if (
|
if (actionInfo.metadata.hasInput) {
|
||||||
allowedStatuses[actionInfo.metadata.allowedStatuses].has(pkgInfo.status)
|
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
||||||
) {
|
label: actionInfo.metadata.name as i18nKey,
|
||||||
if (actionInfo.metadata.hasInput) {
|
data,
|
||||||
this.formDialog.open<PackageActionData>(ActionInputModal, {
|
})
|
||||||
label: actionInfo.metadata.name as i18nKey,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (actionInfo.metadata.warning) {
|
|
||||||
this.dialog
|
|
||||||
.openConfirm({
|
|
||||||
label: 'Warning',
|
|
||||||
size: 's',
|
|
||||||
data: {
|
|
||||||
no: 'Cancel',
|
|
||||||
yes: 'Run',
|
|
||||||
content: actionInfo.metadata.warning as i18nKey,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe(() => this.execute(pkgInfo.id, null, actionInfo.id))
|
|
||||||
} else {
|
|
||||||
this.execute(pkgInfo.id, null, actionInfo.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const statuses = [...allowedStatuses[actionInfo.metadata.allowedStatuses]]
|
if (actionInfo.metadata.warning) {
|
||||||
const last = statuses.pop()
|
this.dialog
|
||||||
let statusesStr = statuses.join(', ')
|
.openConfirm({
|
||||||
if (statuses.length) {
|
label: 'Warning',
|
||||||
if (statuses.length > 1) {
|
size: 's',
|
||||||
// oxford comma
|
data: {
|
||||||
statusesStr += ','
|
no: 'Cancel',
|
||||||
}
|
yes: 'Run',
|
||||||
statusesStr += ` or ${last}`
|
content: actionInfo.metadata.warning as i18nKey,
|
||||||
} else if (last) {
|
},
|
||||||
statusesStr = last
|
})
|
||||||
|
.pipe(filter(Boolean))
|
||||||
|
.subscribe(() => this.execute(pkgInfo.id, null, actionInfo.id))
|
||||||
|
} else {
|
||||||
|
this.execute(pkgInfo.id, null, actionInfo.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialog
|
|
||||||
.openAlert(
|
|
||||||
`${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey,
|
|
||||||
{ label: 'Forbidden' },
|
|
||||||
)
|
|
||||||
.pipe(filter(Boolean))
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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