fix: i18n after synced

This commit is contained in:
Louis 2025-06-26 22:17:47 +07:00
parent e1b6690763
commit 74fafac5fa
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
8 changed files with 467 additions and 388 deletions

View File

@ -10,24 +10,24 @@
* --help Show this help message
*/
const fs = require("fs")
const path = require("path")
const fs = require('fs')
const path = require('path')
// Parse command-line arguments
const args = process.argv.slice(2).reduce((acc, arg) => {
if (arg === "--help") {
acc.help = true
} else if (arg.startsWith("--locale=")) {
acc.locale = arg.split("=")[1]
} else if (arg.startsWith("--file=")) {
acc.file = arg.split("=")[1]
}
return acc
if (arg === '--help') {
acc.help = true
} else if (arg.startsWith('--locale=')) {
acc.locale = arg.split('=')[1]
} else if (arg.startsWith('--file=')) {
acc.file = arg.split('=')[1]
}
return acc
}, {})
// Display help information
if (args.help) {
console.log(`
console.log(`
Find missing i18n translations in Jan
A useful script to identify whether the i18n keys used in component files exist in all language files.
@ -43,264 +43,303 @@ Options:
Output:
- Generate a report of missing translations
`)
process.exit(0)
process.exit(0)
}
// Directories to traverse and their corresponding locales
const DIRS = {
components: {
path: path.join(__dirname, "../web-app/src/components"),
localesDir: path.join(__dirname, "../web-app/src/locales"),
},
containers: {
path: path.join(__dirname, "../web-app/src/containers"),
localesDir: path.join(__dirname, "../web-app/src/locales"),
},
routes: {
path: path.join(__dirname, "../web-app/src/routes"),
localesDir: path.join(__dirname, "../web-app/src/locales"),
},
components: {
path: path.join(__dirname, '../web-app/src/components'),
localesDir: path.join(__dirname, '../web-app/src/locales'),
},
containers: {
path: path.join(__dirname, '../web-app/src/containers'),
localesDir: path.join(__dirname, '../web-app/src/locales'),
},
routes: {
path: path.join(__dirname, '../web-app/src/routes'),
localesDir: path.join(__dirname, '../web-app/src/locales'),
},
}
// Regular expressions to match i18n keys
const i18nPatterns = [
/{t\("([^"]+)"\)}/g, // Match {t("key")} format
/i18nKey="([^"]+)"/g, // Match i18nKey="key" format
/\bt\(\s*["']([^"']+)["']\s*(?:,\s*[^)]+)?\)/g, // Match t("key") format with optional parameters - simplified and more robust
/{t\("([^"]+)"\)}/g, // Match {t("key")} format
/i18nKey="([^"]+)"/g, // Match i18nKey="key" format
/\bt\(\s*["']([^"']+)["']\s*(?:,\s*[^)]+)?\)/g, // Match t("key") format with optional parameters - simplified and more robust
]
// Get all language directories for a specific locales directory
function getLocaleDirs(localesDir) {
try {
const allLocales = fs.readdirSync(localesDir).filter((file) => {
const stats = fs.statSync(path.join(localesDir, file))
return stats.isDirectory() // Do not exclude any language directories
})
try {
const allLocales = fs.readdirSync(localesDir).filter((file) => {
const stats = fs.statSync(path.join(localesDir, file))
return stats.isDirectory() // Do not exclude any language directories
})
// Filter to a specific language if specified
return args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
} catch (error) {
if (error.code === "ENOENT") {
console.warn(`Warning: Locales directory not found: ${localesDir}`)
return []
}
throw error
}
// Filter to a specific language if specified
return args.locale
? allLocales.filter((locale) => locale === args.locale)
: allLocales
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(`Warning: Locales directory not found: ${localesDir}`)
return []
}
throw error
}
}
// Get the value from JSON by path
function getValueByPath(obj, path) {
const parts = path.split(".")
let current = obj
const parts = path.split('.')
let current = obj
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
return current
return current
}
// Check if the key exists in all language files, return a list of missing language files
function checkKeyInLocales(key, localeDirs, localesDir) {
// Handle namespace:key format (e.g., "common:save" or "settings:general")
let namespace, keyPath
// Handle namespace:key format (e.g., "common:save" or "settings:general")
let namespace, keyPath
if (key.includes(":")) {
[namespace, keyPath] = key.split(":", 2)
} else if (key.includes(".")) {
// Handle namespace.key format
const parts = key.split(".")
if (key.includes(':')) {
;[namespace, keyPath] = key.split(':', 2)
} else if (key.includes('.')) {
// Handle namespace.key format
const parts = key.split('.')
// Check if the first part is a known namespace
const knownNamespaces = ['common', 'settings', 'systemMonitor', 'chat', 'hub', 'providers', 'assistants', 'mcpServers', 'mcp-servers', 'toolApproval', 'tool-approval', 'updater', 'setup', 'logs', 'provider']
// Check if the first part is a known namespace
const knownNamespaces = [
'common',
'settings',
'systemMonitor',
'chat',
'hub',
'providers',
'assistants',
'mcpServers',
'mcp-servers',
'toolApproval',
'tool-approval',
'updater',
'setup',
'logs',
'provider',
'model-errors',
]
if (knownNamespaces.includes(parts[0])) {
namespace = parts[0]
keyPath = parts.slice(1).join(".")
} else {
// Default to common namespace if no known namespace is found
namespace = "common"
keyPath = key
}
} else {
// No dots, default to common namespace
namespace = "common"
keyPath = key
}
if (knownNamespaces.includes(parts[0])) {
namespace = parts[0]
keyPath = parts.slice(1).join('.')
} else {
// Default to common namespace if no known namespace is found
namespace = 'common'
keyPath = key
}
} else {
// No dots, default to common namespace
namespace = 'common'
keyPath = key
}
const missingLocales = []
const missingLocales = []
// Map namespace to actual filename
const namespaceToFile = {
'systemMonitor': 'system-monitor',
'mcpServers': 'mcp-servers',
'mcp-servers': 'mcp-servers',
'toolApproval': 'tool-approval',
'tool-approval': 'tool-approval'
}
// Map namespace to actual filename
const namespaceToFile = {
'systemMonitor': 'system-monitor',
'mcpServers': 'mcp-servers',
'mcp-servers': 'mcp-servers',
'toolApproval': 'tool-approval',
'tool-approval': 'tool-approval',
'model-errors': 'model-errors',
}
const fileName = namespaceToFile[namespace] || namespace
const fileName = namespaceToFile[namespace] || namespace
localeDirs.forEach((locale) => {
const filePath = path.join(localesDir, locale, `${fileName}.json`)
if (!fs.existsSync(filePath)) {
missingLocales.push(`${locale}/${fileName}.json`)
return
}
localeDirs.forEach((locale) => {
const filePath = path.join(localesDir, locale, `${fileName}.json`)
if (!fs.existsSync(filePath)) {
missingLocales.push(`${locale}/${fileName}.json`)
return
}
try {
const json = JSON.parse(fs.readFileSync(filePath, "utf8"))
try {
const json = JSON.parse(fs.readFileSync(filePath, 'utf8'))
// Jan's localization files have flat structure
// e.g., common.json has { "save": "Save", "cancel": "Cancel" }
// not nested like { "common": { "save": "Save" } }
const valueToCheck = getValueByPath(json, keyPath)
// Jan's localization files have flat structure
// e.g., common.json has { "save": "Save", "cancel": "Cancel" }
// not nested like { "common": { "save": "Save" } }
const valueToCheck = getValueByPath(json, keyPath)
if (valueToCheck === undefined) {
missingLocales.push(`${locale}/${fileName}.json`)
}
} catch (error) {
console.warn(`Warning: Could not parse ${filePath}: ${error.message}`)
missingLocales.push(`${locale}/${fileName}.json`)
}
})
if (valueToCheck === undefined) {
missingLocales.push(`${locale}/${fileName}.json`)
}
} catch (error) {
console.warn(`Warning: Could not parse ${filePath}: ${error.message}`)
missingLocales.push(`${locale}/${fileName}.json`)
}
})
return missingLocales
return missingLocales
}
// Recursively traverse the directory
function findMissingI18nKeys() {
const results = []
const results = []
function walk(dir, baseDir, localeDirs, localesDir) {
if (!fs.existsSync(dir)) {
console.warn(`Warning: Directory not found: ${dir}`)
return
}
function walk(dir, baseDir, localeDirs, localesDir) {
if (!fs.existsSync(dir)) {
console.warn(`Warning: Directory not found: ${dir}`)
return
}
const files = fs.readdirSync(dir)
const files = fs.readdirSync(dir)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
for (const file of files) {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
// Exclude test files, __mocks__ directory, and node_modules
if (filePath.includes(".test.") ||
filePath.includes("__mocks__") ||
filePath.includes("node_modules") ||
filePath.includes(".spec.")) {
continue
}
// Exclude test files, __mocks__ directory, and node_modules
if (
filePath.includes('.test.') ||
filePath.includes('__mocks__') ||
filePath.includes('node_modules') ||
filePath.includes('.spec.')
) {
continue
}
if (stat.isDirectory()) {
walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories
} else if (stat.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(path.extname(filePath))) {
const content = fs.readFileSync(filePath, "utf8")
if (stat.isDirectory()) {
walk(filePath, baseDir, localeDirs, localesDir) // Recursively traverse subdirectories
} else if (
stat.isFile() &&
['.ts', '.tsx', '.js', '.jsx'].includes(path.extname(filePath))
) {
const content = fs.readFileSync(filePath, 'utf8')
// Match all i18n keys
for (const pattern of i18nPatterns) {
let match
while ((match = pattern.exec(content)) !== null) {
const key = match[1]
// Match all i18n keys
for (const pattern of i18nPatterns) {
let match
while ((match = pattern.exec(content)) !== null) {
const key = match[1]
// Skip empty keys or keys that look like variables/invalid
if (!key ||
key.includes("${") ||
key.includes("{{") ||
key.startsWith("$") ||
key.length < 2 ||
key === "." ||
key === "," ||
key === "-" ||
!/^[a-zA-Z]/.test(key)) {
continue
}
// Skip empty keys or keys that look like variables/invalid
if (
!key ||
key.includes('${') ||
key.includes('{{') ||
key.startsWith('$') ||
key.length < 2 ||
key === '.' ||
key === ',' ||
key === '-' ||
!/^[a-zA-Z]/.test(key)
) {
continue
}
const missingLocales = checkKeyInLocales(key, localeDirs, localesDir)
if (missingLocales.length > 0) {
results.push({
key,
missingLocales,
file: path.relative(baseDir, filePath),
})
}
}
}
}
}
}
const missingLocales = checkKeyInLocales(
key,
localeDirs,
localesDir
)
if (missingLocales.length > 0) {
results.push({
key,
missingLocales,
file: path.relative(baseDir, filePath),
})
}
}
}
}
}
}
// Walk through all directories
Object.entries(DIRS).forEach(([name, config]) => {
const localeDirs = getLocaleDirs(config.localesDir)
if (localeDirs.length > 0) {
console.log(`\nChecking ${name} directory with ${localeDirs.length} languages: ${localeDirs.join(", ")}`)
walk(config.path, config.path, localeDirs, config.localesDir)
}
})
// Walk through all directories
Object.entries(DIRS).forEach(([name, config]) => {
const localeDirs = getLocaleDirs(config.localesDir)
if (localeDirs.length > 0) {
console.log(
`\nChecking ${name} directory with ${
localeDirs.length
} languages: ${localeDirs.join(', ')}`
)
walk(config.path, config.path, localeDirs, config.localesDir)
}
})
return results
return results
}
// Execute and output the results
function main() {
try {
if (args.locale) {
// Check if the specified locale exists in the locales directory
const localesDir = path.join(__dirname, "../web-app/src/locales")
const localeDirs = getLocaleDirs(localesDir)
try {
if (args.locale) {
// Check if the specified locale exists in the locales directory
const localesDir = path.join(__dirname, '../web-app/src/locales')
const localeDirs = getLocaleDirs(localesDir)
if (!localeDirs.includes(args.locale)) {
console.error(`Error: Language '${args.locale}' not found in ${localesDir}`)
process.exit(1)
}
}
if (!localeDirs.includes(args.locale)) {
console.error(
`Error: Language '${args.locale}' not found in ${localesDir}`
)
process.exit(1)
}
}
const missingKeys = findMissingI18nKeys()
const missingKeys = findMissingI18nKeys()
if (missingKeys.length === 0) {
console.log("\n✅ All i18n keys are present!")
return
}
if (missingKeys.length === 0) {
console.log('\n✅ All i18n keys are present!')
return
}
console.log("\nMissing i18n keys:\n")
console.log('\nMissing i18n keys:\n')
// Group by file for better readability
const groupedByFile = {}
missingKeys.forEach(({ key, missingLocales, file }) => {
if (!groupedByFile[file]) {
groupedByFile[file] = []
}
groupedByFile[file].push({ key, missingLocales })
})
// Group by file for better readability
const groupedByFile = {}
missingKeys.forEach(({ key, missingLocales, file }) => {
if (!groupedByFile[file]) {
groupedByFile[file] = []
}
groupedByFile[file].push({ key, missingLocales })
})
Object.entries(groupedByFile).forEach(([file, keys]) => {
console.log(`📁 File: ${file}`)
keys.forEach(({ key, missingLocales }) => {
console.log(` 🔑 Key: ${key}`)
console.log(" ❌ Missing in:")
missingLocales.forEach((locale) => console.log(` - ${locale}`))
console.log("")
})
console.log("-------------------")
})
Object.entries(groupedByFile).forEach(([file, keys]) => {
console.log(`📁 File: ${file}`)
keys.forEach(({ key, missingLocales }) => {
console.log(` 🔑 Key: ${key}`)
console.log(' ❌ Missing in:')
missingLocales.forEach((locale) => console.log(` - ${locale}`))
console.log('')
})
console.log('-------------------')
})
console.log("\n💡 To fix missing translations:")
console.log("1. Add the missing keys to the appropriate locale files")
console.log("2. Use yq commands for efficient updates:")
console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales/<locale>/<file>.json")
console.log("3. Run this script again to verify all keys are present")
console.log('\n💡 To fix missing translations:')
console.log('1. Add the missing keys to the appropriate locale files')
console.log('2. Use yq commands for efficient updates:')
console.log(
' yq -i \'.namespace.key = "Translation"\' web-app/src/locales/<locale>/<file>.json'
)
console.log('3. Run this script again to verify all keys are present')
// Exit code 1 indicates missing keys
process.exit(1)
} catch (error) {
console.error("Error:", error.message)
console.error(error.stack)
process.exit(1)
}
// Exit code 1 indicates missing keys
process.exit(1)
} catch (error) {
console.error('Error:', error.message)
console.error(error.stack)
process.exit(1)
}
}
main()
main()

View File

@ -10,27 +10,24 @@
* --help Show this help message
*/
const fs = require("fs")
const path = require("path")
const fs = require('fs')
const path = require('path')
// Process command line arguments
const args = process.argv.slice(2).reduce(
(acc, arg) => {
if (arg === "--help") {
acc.help = true
} else if (arg.startsWith("--locale=")) {
acc.locale = arg.split("=")[1]
} else if (arg.startsWith("--file=")) {
acc.file = arg.split("=")[1]
}
return acc
},
{}
)
const args = process.argv.slice(2).reduce((acc, arg) => {
if (arg === '--help') {
acc.help = true
} else if (arg.startsWith('--locale=')) {
acc.locale = arg.split('=')[1]
} else if (arg.startsWith('--file=')) {
acc.file = arg.split('=')[1]
}
return acc
}, {})
// Show help if requested
if (args.help) {
console.log(`
console.log(`
Find Missing Translations for Jan
A utility script to identify missing translations across locale files.
@ -47,207 +44,225 @@ Options:
Output:
- Generates a report of missing translations for the web-app
`)
process.exit(0)
process.exit(0)
}
// Path to the locales directory
const LOCALES_DIR = path.join(__dirname, "../web-app/src/locales")
const LOCALES_DIR = path.join(__dirname, '../web-app/src/locales')
// Recursively find all keys in an object
function findKeys(obj, parentKey = "") {
let keys = []
function findKeys(obj, parentKey = '') {
let keys = []
for (const [key, value] of Object.entries(obj)) {
const currentKey = parentKey ? `${parentKey}.${key}` : key
for (const [key, value] of Object.entries(obj)) {
const currentKey = parentKey ? `${parentKey}.${key}` : key
if (typeof value === "object" && value !== null) {
// If value is an object, recurse
keys = [...keys, ...findKeys(value, currentKey)]
} else {
// If value is a primitive, add the key
keys.push(currentKey)
}
}
if (typeof value === 'object' && value !== null) {
// If value is an object, recurse
keys = [...keys, ...findKeys(value, currentKey)]
} else {
// If value is a primitive, add the key
keys.push(currentKey)
}
}
return keys
return keys
}
// Get value at a dotted path in an object
function getValueAtPath(obj, path) {
const parts = path.split(".")
let current = obj
const parts = path.split('.')
let current = obj
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
return current
return current
}
// Function to check translations
function checkTranslations() {
// Get all locale directories (or filter to the specified locale)
const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
const stats = fs.statSync(path.join(LOCALES_DIR, item))
return stats.isDirectory() && item !== "en" // Exclude English as it's our source
})
// Get all locale directories (or filter to the specified locale)
const allLocales = fs.readdirSync(LOCALES_DIR).filter((item) => {
const stats = fs.statSync(path.join(LOCALES_DIR, item))
return stats.isDirectory() && item !== 'en' // Exclude English as it's our source
})
// Filter to the specified locale if provided
const locales = args.locale ? allLocales.filter((locale) => locale === args.locale) : allLocales
// Filter to the specified locale if provided
const locales = args.locale
? allLocales.filter((locale) => locale === args.locale)
: allLocales
if (args.locale && locales.length === 0) {
console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
process.exit(1)
}
if (args.locale && locales.length === 0) {
console.error(`Error: Locale '${args.locale}' not found in ${LOCALES_DIR}`)
process.exit(1)
}
console.log(`Checking ${locales.length} non-English locale(s): ${locales.join(", ")}`)
console.log(
`Checking ${locales.length} non-English locale(s): ${locales.join(', ')}`
)
// Get all English JSON files
const englishDir = path.join(LOCALES_DIR, "en")
let englishFiles = fs.readdirSync(englishDir).filter((file) => file.endsWith(".json") && !file.startsWith("."))
// Get all English JSON files
const englishDir = path.join(LOCALES_DIR, 'en')
let englishFiles = fs
.readdirSync(englishDir)
.filter((file) => file.endsWith('.json') && !file.startsWith('.'))
// Filter to the specified file if provided
if (args.file) {
if (!englishFiles.includes(args.file)) {
console.error(`Error: File '${args.file}' not found in ${englishDir}`)
process.exit(1)
}
englishFiles = englishFiles.filter((file) => file === args.file)
}
// Filter to the specified file if provided
if (args.file) {
if (!englishFiles.includes(args.file)) {
console.error(`Error: File '${args.file}' not found in ${englishDir}`)
process.exit(1)
}
englishFiles = englishFiles.filter((file) => file === args.file)
}
// Load file contents
let englishFileContents
// Load file contents
let englishFileContents
try {
englishFileContents = englishFiles.map((file) => ({
name: file,
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), "utf8")),
}))
} catch (e) {
console.error(`Error: File '${englishDir}' is not a valid JSON file`)
process.exit(1)
}
try {
englishFileContents = englishFiles.map((file) => ({
name: file,
content: JSON.parse(fs.readFileSync(path.join(englishDir, file), 'utf8')),
}))
} catch (e) {
console.error(`Error: File '${englishDir}' is not a valid JSON file`)
process.exit(1)
}
console.log(
`Checking ${englishFileContents.length} translation file(s): ${englishFileContents.map((f) => f.name).join(", ")}`
)
console.log(
`Checking ${
englishFileContents.length
} translation file(s): ${englishFileContents.map((f) => f.name).join(', ')}`
)
// Results object to store missing translations
const missingTranslations = {}
// Results object to store missing translations
const missingTranslations = {}
// For each locale, check for missing translations
for (const locale of locales) {
missingTranslations[locale] = {}
// For each locale, check for missing translations
for (const locale of locales) {
missingTranslations[locale] = {}
for (const { name, content: englishContent } of englishFileContents) {
const localeFilePath = path.join(LOCALES_DIR, locale, name)
for (const { name, content: englishContent } of englishFileContents) {
const localeFilePath = path.join(LOCALES_DIR, locale, name)
// Check if the file exists in the locale
if (!fs.existsSync(localeFilePath)) {
missingTranslations[locale][name] = { file: "File is missing entirely" }
continue
}
// Check if the file exists in the locale
if (!fs.existsSync(localeFilePath)) {
missingTranslations[locale][name] = { file: 'File is missing entirely' }
continue
}
// Load the locale file
let localeContent
// Load the locale file
let localeContent
try {
localeContent = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))
} catch (e) {
console.error(`Error: File '${localeFilePath}' is not a valid JSON file`)
process.exit(1)
}
try {
localeContent = JSON.parse(fs.readFileSync(localeFilePath, 'utf8'))
} catch (e) {
console.error(
`Error: File '${localeFilePath}' is not a valid JSON file`
)
process.exit(1)
}
// Find all keys in the English file
const englishKeys = findKeys(englishContent)
// Find all keys in the English file
const englishKeys = findKeys(englishContent)
// Check for missing keys in the locale file
const missingKeys = []
// Check for missing keys in the locale file
const missingKeys = []
for (const key of englishKeys) {
const englishValue = getValueAtPath(englishContent, key)
const localeValue = getValueAtPath(localeContent, key)
for (const key of englishKeys) {
const englishValue = getValueAtPath(englishContent, key)
const localeValue = getValueAtPath(localeContent, key)
if (localeValue === undefined) {
missingKeys.push({
key,
englishValue,
})
}
}
if (localeValue === undefined) {
missingKeys.push({
key,
englishValue,
})
}
}
if (missingKeys.length > 0) {
missingTranslations[locale][name] = missingKeys
}
}
}
if (missingKeys.length > 0) {
missingTranslations[locale][name] = missingKeys
}
}
}
return outputResults(missingTranslations)
return outputResults(missingTranslations)
}
// Function to output results
function outputResults(missingTranslations) {
let hasMissingTranslations = false
let hasMissingTranslations = false
console.log(`\nMissing Translations Report:\n`)
console.log(`\nMissing Translations Report:\n`)
for (const [locale, files] of Object.entries(missingTranslations)) {
if (Object.keys(files).length === 0) {
console.log(`${locale}: No missing translations`)
continue
}
for (const [locale, files] of Object.entries(missingTranslations)) {
if (Object.keys(files).length === 0) {
console.log(`${locale}: No missing translations`)
continue
}
hasMissingTranslations = true
console.log(`📝 ${locale}:`)
hasMissingTranslations = true
console.log(`📝 ${locale}:`)
for (const [fileName, missingItems] of Object.entries(files)) {
if (missingItems.file) {
console.log(` - ${fileName}: ${missingItems.file}`)
continue
}
for (const [fileName, missingItems] of Object.entries(files)) {
if (missingItems.file) {
console.log(` - ${fileName}: ${missingItems.file}`)
continue
}
console.log(` - ${fileName}: ${missingItems.length} missing translations`)
console.log(
` - ${fileName}: ${missingItems.length} missing translations`
)
for (const { key, englishValue } of missingItems) {
console.log(` ${key}: "${englishValue}"`)
}
}
for (const { key, englishValue } of missingItems) {
console.log(` ${key}: "${englishValue}"`)
}
}
console.log("")
}
console.log('')
}
return hasMissingTranslations
return hasMissingTranslations
}
// Main function to find missing translations
function findMissingTranslations() {
try {
console.log("Starting translation check for Jan web-app...")
try {
console.log('Starting translation check for Jan web-app...')
const hasMissingTranslations = checkTranslations()
const hasMissingTranslations = checkTranslations()
// Summary
if (!hasMissingTranslations) {
console.log("\n✅ All translations are complete!")
} else {
console.log("\n✏ To add missing translations:")
console.log("1. Add the missing keys to the corresponding locale files")
console.log("2. Translate the English values to the appropriate language")
console.log("3. You can use yq commands to update JSON files efficiently:")
console.log(" yq -i '.namespace.key = \"Translation\"' web-app/src/locales/<locale>/<file>.json")
console.log("4. Run this script again to verify all translations are complete")
// Exit with error code to fail CI checks
process.exit(1)
}
} catch (error) {
console.error("Error:", error.message)
console.error(error.stack)
process.exit(1)
}
// Summary
if (!hasMissingTranslations) {
console.log('\n✅ All translations are complete!')
} else {
console.log('\n✏ To add missing translations:')
console.log('1. Add the missing keys to the corresponding locale files')
console.log('2. Translate the English values to the appropriate language')
console.log(
'3. You can use yq commands to update JSON files efficiently:'
)
console.log(
' yq -i \'.namespace.key = "Translation"\' web-app/src/locales/<locale>/<file>.json'
)
console.log(
'4. Run this script again to verify all translations are complete'
)
// Exit with error code to fail CI checks
process.exit(1)
}
} catch (error) {
console.error('Error:', error.message)
console.error(error.stack)
process.exit(1)
}
}
// Run the main function
findMissingTranslations()
findMissingTranslations()

View File

@ -1,4 +1,3 @@
import { t } from 'i18next'
import {
Dialog,
DialogContent,
@ -10,8 +9,10 @@ import {
import { Button } from '@/components/ui/button'
import { useContextSizeApproval } from '@/hooks/useModelContextApproval'
import { useTranslation } from '@/i18n'
export default function OutOfContextPromiseModal() {
const { t } = useTranslation()
const { isModalOpen, modalProps, setModalOpen } = useContextSizeApproval()
if (!modalProps) {
return null
@ -37,21 +38,13 @@ export default function OutOfContextPromiseModal() {
<Dialog open={isModalOpen} onOpenChange={handleDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{t('outOfContextError.title', 'Out of context error')}
</DialogTitle>
<DialogTitle>{t('model-errors:title')}</DialogTitle>
</DialogHeader>
<DialogDescription>
{t(
'outOfContextError.description',
'This chat is reaching the AIs memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computers memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.'
)}
{t('model-errors:description')}
<br />
<br />
{t(
'outOfContextError.increaseContextSizeDescription',
'Do you want to increase the context size?'
)}
{t('model-errors:increaseContextSizeDescription')}
</DialogDescription>
<DialogFooter className="flex gap-2">
<Button
@ -61,7 +54,7 @@ export default function OutOfContextPromiseModal() {
handleContextShift()
}}
>
{t('outOfContextError.truncateInput', 'Truncate Input')}
{t('model-errors:truncateInput')}
</Button>
<Button
asChild
@ -70,10 +63,7 @@ export default function OutOfContextPromiseModal() {
}}
>
<span className="text-main-view-fg/70">
{t(
'outOfContextError.increaseContextSize',
'Increase Context Size'
)}
{t('model-errors:increaseContextSize')}
</span>
</Button>
</DialogFooter>

View File

@ -0,0 +1,7 @@
{
"title": "Out of context error",
"description": "This chat is reaching the AIs memory limit, like a whiteboard filling up. We can expand the memory window (called context size) so it remembers more, but it may use more of your computers memory. We can also truncate the input, which means it will forget some of the chat history to make room for new messages.",
"increaseContextSizeDescription": "Do you want to increase the context size?",
"truncateInput": "Truncate Input",
"increaseContextSize": "Increase Context Size"
}

View File

@ -0,0 +1,7 @@
{
"title": "Kesalahan kehabisan konteks",
"description": "Obrolan ini hampir mencapai batas memori AI, seperti papan tulis yang mulai penuh. Kita bisa memperluas jendela memori (disebut ukuran konteks) agar AI dapat mengingat lebih banyak, tetapi ini mungkin akan menggunakan lebih banyak memori komputer Anda. Kita juga bisa memotong input, artinya sebagian riwayat obrolan akan dilupakan untuk memberi ruang pada pesan baru.",
"increaseContextSizeDescription": "Apakah Anda ingin memperbesar ukuran konteks?",
"truncateInput": "Potong Input",
"increaseContextSize": "Perbesar Ukuran Konteks"
}

View File

@ -0,0 +1,7 @@
{
"title": "Lỗi vượt quá ngữ cảnh",
"description": "Cuộc trò chuyện này đang đạt đến giới hạn bộ nhớ của AI, giống như một bảng trắng sắp đầy. Chúng ta có thể mở rộng cửa sổ bộ nhớ (gọi là kích thước ngữ cảnh) để AI nhớ được nhiều hơn, nhưng điều này có thể sử dụng nhiều bộ nhớ máy tính của bạn hơn. Chúng ta cũng có thể cắt bớt đầu vào, nghĩa là AI sẽ quên một phần lịch sử trò chuyện để dành chỗ cho các tin nhắn mới.",
"increaseContextSizeDescription": "Bạn có muốn tăng kích thước ngữ cảnh không?",
"truncateInput": "Cắt bớt đầu vào",
"increaseContextSize": "Tăng kích thước ngữ cảnh"
}

View File

@ -0,0 +1,7 @@
{
"title": "超出上下文错误",
"description": "此对话已接近 AI 的记忆上限,就像白板快被填满一样。我们可以扩展记忆窗口(称为上下文大小),这样它能记住更多内容,但可能会占用你电脑更多内存。我们也可以截断输入,这意味着它会遗忘部分聊天记录,为新消息腾出空间。",
"increaseContextSizeDescription": "你想要增加上下文大小吗?",
"truncateInput": "截断输入",
"increaseContextSize": "增加上下文大小"
}

View File

@ -0,0 +1,7 @@
{
"title": "超出上下文錯誤",
"description": "此對話已接近 AI 的記憶上限,就像白板快被填滿一樣。我們可以擴大記憶視窗(稱為上下文大小),讓它能記住更多內容,但這可能會佔用你電腦更多記憶體。我們也可以截斷輸入,這表示它會忘記部分對話歷史,以便為新訊息騰出空間。",
"increaseContextSizeDescription": "你想要增加上下文大小嗎?",
"truncateInput": "截斷輸入",
"increaseContextSize": "增加上下文大小"
}