Enhance i18n and add missing i18n for all component (#5314)

* Refactor translation imports and update text for localization across settings and system monitor routes

- Changed translation import from 'react-i18next' to '@/i18n/react-i18next-compat' in multiple files.
- Updated various text strings to use translation keys for better localization support in:
  - Local API Server settings
  - MCP Servers settings
  - Privacy settings
  - Provider settings
  - Shortcuts settings
  - System Monitor
  - Thread details
- Ensured consistent use of translation keys for all user-facing text.

Update web-app/src/routes/settings/appearance.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

Update web-app/src/routes/settings/appearance.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

Update web-app/src/locales/vn/settings.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

Update web-app/src/containers/dialogs/DeleteMCPServerConfirm.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

Update web-app/src/locales/id/common.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Add Chinese (Simplified and Traditional) localization files for various components

- Created `tools.json`, `updater.json`, `assistants.json`, `chat.json`, `common.json`, `hub.json`, `logs.json`, `mcp-servers.json`, `provider.json`, `providers.json`, `settings.json`, `setup.json`, `system-monitor.json`, `tool-approval.json` in both `zh-CN` and `zh-TW` locales.
- Added translations for tool approval, updater notifications, assistant management, chat interface, common UI elements, hub interactions, logging messages, MCP server configurations, provider management, settings options, setup instructions, and system monitoring.

* Refactor localization strings for improved clarity and consistency in English, Indonesian, and Vietnamese settings files

* Fix missing key and reword

* fix pr comment

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Sam Hoang Van 2025-06-20 15:33:54 +07:00 committed by GitHub
parent ebd9e0863e
commit c32dd092d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
133 changed files with 5526 additions and 883 deletions

View File

@ -0,0 +1,306 @@
/**
* Script to find missing i18n keys in Jan components
*
* Usage:
* node scripts/find-missing-i18n-key.js [options]
*
* Options:
* --locale=<locale> Only check a specific locale (e.g. --locale=id)
* --file=<file> Only check a specific file (e.g. --file=common.json)
* --help Show this help message
*/
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
}, {})
// Display help information
if (args.help) {
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.
Usage:
node scripts/find-missing-i18n-key.js [options]
Options:
--locale=<locale> Only check a specific language (e.g., --locale=id)
--file=<file> Only check a specific file (e.g., --file=common.json)
--help Display help information
Output:
- Generate a report of missing translations
`)
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"),
},
}
// 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
]
// 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
})
// 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
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
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
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']
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 = []
// Map namespace to actual filename
const namespaceToFile = {
'systemMonitor': 'system-monitor',
'mcpServers': 'mcp-servers',
'mcp-servers': 'mcp-servers',
'toolApproval': 'tool-approval',
'tool-approval': 'tool-approval'
}
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
}
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)
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
}
// Recursively traverse the directory
function findMissingI18nKeys() {
const results = []
function walk(dir, baseDir, localeDirs, localesDir) {
if (!fs.existsSync(dir)) {
console.warn(`Warning: Directory not found: ${dir}`)
return
}
const files = fs.readdirSync(dir)
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
}
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]
// 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),
})
}
}
}
}
}
}
// 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
}
// 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)
if (!localeDirs.includes(args.locale)) {
console.error(`Error: Language '${args.locale}' not found in ${localesDir}`)
process.exit(1)
}
}
const missingKeys = findMissingI18nKeys()
if (missingKeys.length === 0) {
console.log("\n✅ All i18n keys are present!")
return
}
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 })
})
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")
// Exit code 1 indicates missing keys
process.exit(1)
} catch (error) {
console.error("Error:", error.message)
console.error(error.stack)
process.exit(1)
}
}
main()

View File

@ -0,0 +1,253 @@
/**
* Script to find missing translations in locale files for Jan
*
* Usage:
* node scripts/find-missing-translations.js [options]
*
* Options:
* --locale=<locale> Only check a specific locale (e.g. --locale=id)
* --file=<file> Only check a specific file (e.g. --file=common.json)
* --help Show this help message
*/
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
},
{}
)
// Show help if requested
if (args.help) {
console.log(`
Find Missing Translations for Jan
A utility script to identify missing translations across locale files.
Compares non-English locale files to the English ones to find any missing keys.
Usage:
node scripts/find-missing-translations.js [options]
Options:
--locale=<locale> Only check a specific locale (e.g. --locale=id)
--file=<file> Only check a specific file (e.g. --file=common.json)
--help Show this help message
Output:
- Generates a report of missing translations for the web-app
`)
process.exit(0)
}
// Path to the locales directory
const LOCALES_DIR = path.join(__dirname, "../web-app/src/locales")
// Recursively find all keys in an object
function findKeys(obj, parentKey = "") {
let keys = []
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)
}
}
return keys
}
// Get value at a dotted path in an object
function getValueAtPath(obj, path) {
const parts = path.split(".")
let current = obj
for (const part of parts) {
if (current === undefined || current === null) {
return undefined
}
current = current[part]
}
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
})
// 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)
}
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("."))
// 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
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(", ")}`
)
// Results object to store missing translations
const missingTranslations = {}
// 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)
// 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
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)
// 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)
if (localeValue === undefined) {
missingKeys.push({
key,
englishValue,
})
}
}
if (missingKeys.length > 0) {
missingTranslations[locale][name] = missingKeys
}
}
}
return outputResults(missingTranslations)
}
// Function to output results
function outputResults(missingTranslations) {
let hasMissingTranslations = false
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
}
hasMissingTranslations = true
console.log(`📝 ${locale}:`)
for (const [fileName, missingItems] of Object.entries(files)) {
if (missingItems.file) {
console.log(` - ${fileName}: ${missingItems.file}`)
continue
}
console.log(` - ${fileName}: ${missingItems.length} missing translations`)
for (const { key, englishValue } of missingItems) {
console.log(` ${key}: "${englishValue}"`)
}
}
console.log("")
}
return hasMissingTranslations
}
// Main function to find missing translations
function findMissingTranslations() {
try {
console.log("Starting translation check for Jan web-app...")
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)
}
}
// Run the main function
findMissingTranslations()

View File

@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
function Dialog({
...props
@ -58,6 +59,7 @@ function DialogContent({
'aria-describedby': ariaDescribedBy,
...props
}: DialogContentProps) {
const { t } = useTranslation()
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
@ -74,7 +76,7 @@ function DialogContent({
{showCloseButton && (
<DialogPrimitive.Close className="data-[state=open]:text-main-view-fg/50 absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-0 focus:outline-0 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 cursor-pointer">
<XIcon />
<span className="sr-only">Close</span>
<span className="sr-only">{t('close')}</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>

View File

@ -3,6 +3,7 @@ import * as SheetPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
@ -50,6 +51,7 @@ function SheetContent({
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: 'top' | 'right' | 'bottom' | 'left'
}) {
const { t } = useTranslation()
return (
<SheetPortal>
<SheetOverlay />
@ -72,7 +74,7 @@ function SheetContent({
{children}
<SheetPrimitive.Close className="absolute top-4 text-main-view-fg right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-0 disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
<span className="sr-only">{t('close')}</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>

View File

@ -1,7 +1,8 @@
import { Input } from '@/components/ui/input'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { useState, useEffect } from 'react'
import { useState, useEffect, useCallback } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface ApiKeyInputProps {
showError?: boolean
@ -16,23 +17,24 @@ export function ApiKeyInput({
const [inputValue, setInputValue] = useState(apiKey.toString())
const [showPassword, setShowPassword] = useState(false)
const [error, setError] = useState('')
const { t } = useTranslation()
const validateApiKey = (value: string) => {
const validateApiKey = useCallback((value: string) => {
if (!value || value.trim().length === 0) {
setError('API Key is required')
setError(t('common:apiKeyRequired'))
onValidationChange?.(false)
return false
}
setError('')
onValidationChange?.(true)
return true
}
}, [onValidationChange, t])
useEffect(() => {
if (showError) {
validateApiKey(inputValue)
}
}, [showError, inputValue])
}, [showError, inputValue, validateApiKey])
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
@ -67,7 +69,7 @@ export function ApiKeyInput({
? 'border-1 border-destructive focus:border-destructive focus:ring-destructive'
: ''
}`}
placeholder="Enter API Key"
placeholder={t('common:enterApiKey')}
/>
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
<button

View File

@ -23,7 +23,7 @@ import {
IconPlayerStopFilled,
IconX,
} from '@tabler/icons-react'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useModelProvider } from '@/hooks/useModelProvider'
@ -382,7 +382,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
// When Shift+Enter is pressed, a new line is added (default behavior)
}
}}
placeholder={t('common.placeholder.chatInput')}
placeholder={t('common:placeholder.chatInput')}
autoFocus
spellCheck={spellCheckChatInput}
data-gramm={spellCheckChatInput}
@ -440,7 +440,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
</div>
</TooltipTrigger>
<TooltipContent>
<p>Vision</p>
<p>{t('vision')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -457,7 +457,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
</div>
</TooltipTrigger>
<TooltipContent>
<p>Embeddings</p>
<p>{t('embeddings')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -513,7 +513,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
</div>
</TooltipTrigger>
<TooltipContent>
<p>Tools</p>
<p>{t('tools')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -547,7 +547,7 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => {
</div>
</TooltipTrigger>
<TooltipContent>
<p>Reasoning</p>
<p>{t('reasoning')}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -2,9 +2,11 @@ import { Skeleton } from '@/components/ui/skeleton'
import { useAppearance } from '@/hooks/useAppearance'
import { cn } from '@/lib/utils'
import { IconCircleCheckFilled } from '@tabler/icons-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function ChatWidthSwitcher() {
const { chatWidth, setChatWidth } = useAppearance()
const { t } = useTranslation()
return (
<div className="flex gap-4">
@ -16,7 +18,7 @@ export function ChatWidthSwitcher() {
onClick={() => setChatWidth('compact')}
>
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
<span className="font-medium text-xs font-sans">Compact Width</span>
<span className="font-medium text-xs font-sans">{t('common:compactWidth')}</span>
{chatWidth === 'compact' && (
<IconCircleCheckFilled className="size-4 text-accent" />
)}
@ -27,7 +29,7 @@ export function ChatWidthSwitcher() {
<Skeleton className="h-2 w-full rounded-full" />
<Skeleton className="h-2 w-full rounded-full" />
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
<span className="text-main-view-fg/50">Ask me anything...</span>
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
</div>
</div>
</div>
@ -40,7 +42,7 @@ export function ChatWidthSwitcher() {
onClick={() => setChatWidth('full')}
>
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
<span className="font-medium text-xs font-sans">Full Width</span>
<span className="font-medium text-xs font-sans">{t('common:fullWidth')}</span>
{chatWidth === 'full' && (
<IconCircleCheckFilled className="size-4 text-accent" />
)}
@ -51,7 +53,7 @@ export function ChatWidthSwitcher() {
<Skeleton className="h-2 w-full rounded-full" />
<Skeleton className="h-2 w-full rounded-full" />
<div className="bg-main-view-fg/10 h-8 px-4 w-full flex-shrink-0 border-none resize-none outline-0 rounded-2xl flex items-center">
<span className="text-main-view-fg/50">Ask me anything...</span>
<span className="text-main-view-fg/50">{t('common:placeholder.chatInput')}</span>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
import { RenderMarkdown } from './RenderMarkdown'
import { useTranslation } from '@/i18n/react-i18next-compat'
const EXAMPLE_CODE = `\`\`\`typescript
// Example code for preview
@ -12,10 +13,11 @@ console.log(message); // Outputs: Hello, Jan!
\`\`\``
export function CodeBlockExample() {
const { t } = useTranslation()
return (
<div className="w-full overflow-hidden border border-main-view-fg/10 rounded-md my-2">
<div className="flex items-center justify-between px-4 py-2 bg-main-view-fg/10">
<span className="font-medium text-xs font-sans">Preview</span>
<span className="font-medium text-xs font-sans">{t('preview')}</span>
</div>
<div className="overflow-auto p-2">
<RenderMarkdown content={EXAMPLE_CODE} />

View File

@ -7,9 +7,11 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function ColorPickerAppBgColor() {
const { appBgColor, setAppBgColor } = useAppearance()
const { t } = useTranslation()
const predefineAppBgColor: RgbaColor[] = [
{
@ -80,7 +82,7 @@ export function ColorPickerAppBgColor() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Pick Color Window Background"
title={t('common:pickColorWindowBackground')}
className="size-6 cursor-pointer flex items-center justify-center rounded-sm hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out data-[state=open]:bg-main-view-fg/10"
>
<IconColorPicker size={18} className="text-main-view-fg/50" />

View File

@ -14,8 +14,10 @@ import { DownloadEvent, DownloadState, events, AppEvent } from '@janhq/core'
import { IconDownload, IconX } from '@tabler/icons-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function DownloadManagement() {
const { t } = useTranslation()
const { setProviders } = useModelProvider()
const { open: isLeftPanelOpen } = useLeftPanel()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
@ -67,20 +69,20 @@ export function DownloadManagement() {
isDownloading: false,
downloadProgress: 1,
}))
toast.success('App Update Downloaded', {
description: 'The app update has been downloaded successfully.',
toast.success(t('common:toast.appUpdateDownloaded.title'), {
description: t('common:toast.appUpdateDownloaded.description'),
})
}, [])
}, [t])
const onAppUpdateDownloadError = useCallback(() => {
setAppUpdateState((prev) => ({
...prev,
isDownloading: false,
}))
toast.error('App Update Download Failed', {
description: 'Failed to download the app update. Please try again.',
toast.error(t('common:toast.appUpdateDownloadFailed.title'), {
description: t('common:toast.appUpdateDownloadFailed.description'),
})
}, [])
}, [t])
const downloadProcesses = useMemo(() => {
// Get downloads with progress data
@ -178,12 +180,12 @@ export function DownloadManagement() {
removeDownload(state.modelId)
removeLocalDownloadingModel(state.modelId)
getProviders().then(setProviders)
toast.success('Download Complete', {
toast.success(t('common:toast.downloadComplete.title'), {
id: 'download-complete',
description: `The model ${state.modelId} has been downloaded`,
description: t('common:toast.downloadComplete.description', { modelId: state.modelId }),
})
},
[removeDownload, removeLocalDownloadingModel, setProviders]
[removeDownload, removeLocalDownloadingModel, setProviders, t]
)
useEffect(() => {
@ -238,7 +240,7 @@ export function DownloadManagement() {
<div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
{downloadCount}
</div>
<p className="text-left-panel-fg/80 font-medium">Downloads</p>
<p className="text-left-panel-fg/80 font-medium">{t('downloads')}</p>
<div className="mt-2 flex items-center justify-between space-x-2">
<Progress value={overallProgress * 100} />
<span className="text-xs font-medium text-left-panel-fg/80 shrink-0">
@ -270,7 +272,7 @@ export function DownloadManagement() {
>
<div className="flex flex-col">
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
<p className="text-xs text-main-view-fg/70">Downloading</p>
<p className="text-xs text-main-view-fg/70">{t('downloading')}</p>
</div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{appUpdateState.isDownloading && (
@ -307,10 +309,9 @@ export function DownloadManagement() {
title="Cancel download"
onClick={() => {
abortDownload(download.name).then(() => {
toast.info('Download Cancelled', {
toast.info(t('common:toast.downloadCancelled.title'), {
id: 'cancel-download',
description:
'The download process was cancelled',
description: t('common:toast.downloadCancelled.description'),
})
if (downloadProcesses.length === 0) {
setIsPopoverOpen(false)

View File

@ -17,6 +17,7 @@ import ProvidersAvatar from '@/containers/ProvidersAvatar'
import { Fzf } from 'fzf'
import { localStorageKey } from '@/constants/localStorage'
import { isProd } from '@/lib/version'
import { useTranslation } from '@/i18n/react-i18next-compat'
type DropdownModelProviderProps = {
model?: ThreadModel
@ -68,6 +69,7 @@ const DropdownModelProvider = ({
const [displayModel, setDisplayModel] = useState<string>('')
const { updateCurrentThreadModel } = useThreads()
const navigate = useNavigate()
const { t } = useTranslation()
// Search state
const [open, setOpen] = useState(false)
@ -118,9 +120,9 @@ const DropdownModelProvider = ({
if (selectedProvider && selectedModel) {
setDisplayModel(selectedModel.id)
} else {
setDisplayModel('Select a model')
setDisplayModel(t('common:selectAModel'))
}
}, [selectedProvider, selectedModel])
}, [selectedProvider, selectedModel, t])
// Reset search value when dropdown closes
const onOpenChange = useCallback((open: boolean) => {
@ -307,7 +309,7 @@ const DropdownModelProvider = ({
ref={searchInputRef}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Search models..."
placeholder={t('common:searchModels')}
className="text-sm font-normal outline-0"
/>
{searchValue.length > 0 && (
@ -325,7 +327,7 @@ const DropdownModelProvider = ({
<div className="max-h-[320px] overflow-y-auto">
{Object.keys(groupedItems).length === 0 && searchValue ? (
<div className="py-3 px-4 text-sm text-main-view-fg/60">
No models found for "{searchValue}"
{t('common:noModelsFoundFor', { searchValue })}
</div>
) : (
<div className="py-1">

View File

@ -14,6 +14,7 @@ import { useToolAvailable } from '@/hooks/useToolAvailable'
import React from 'react'
import { useAppState } from '@/hooks/useAppState'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface DropdownToolsAvailableProps {
children: (isOpen: boolean, toolsCount: number) => React.ReactNode
@ -28,6 +29,7 @@ export default function DropdownToolsAvailable({
}: DropdownToolsAvailableProps) {
const { tools } = useAppState()
const [isOpen, setIsOpen] = useState(false)
const { t } = useTranslation()
const handleOpenChange = (open: boolean) => {
setIsOpen(open)
@ -96,7 +98,7 @@ export default function DropdownToolsAvailable({
<DropdownMenu onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>{renderTrigger()}</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="max-w-64">
<DropdownMenuItem disabled>No tools available</DropdownMenuItem>
<DropdownMenuItem disabled>{t('common:noToolsAvailable')}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)

View File

@ -6,20 +6,22 @@ import {
} from '@/components/ui/dropdown-menu'
import { fontSizeOptions, useAppearance } from '@/hooks/useAppearance'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function FontSizeSwitcher() {
const { fontSize, setFontSize } = useAppearance()
const { t } = useTranslation()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<span
title="Adjust Font Size"
title={t('common:adjustFontSize')}
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
>
{fontSizeOptions.find(
(item: { value: string; label: string }) => item.value === fontSize
)?.label || 'Medium'}
)?.label || t('common:medium')}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-24">

View File

@ -1,5 +1,5 @@
import { useGeneralSetting } from '@/hooks/useGeneralSetting'
import { useTranslation } from 'react-i18next'
import { useAppTranslation } from '@/i18n'
import {
DropdownMenu,
DropdownMenuContent,
@ -12,10 +12,12 @@ const LANGUAGES = [
{ value: 'en', label: 'English' },
{ value: 'id', label: 'Bahasa' },
{ value: 'vn', label: 'Tiếng Việt' },
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁體中文' },
]
export default function LanguageSwitcher() {
const { i18n } = useTranslation()
const { i18n, t } = useAppTranslation()
const { setCurrentLanguage, currentLanguage } = useGeneralSetting()
const changeLanguage = (lng: string) => {
@ -27,13 +29,13 @@ export default function LanguageSwitcher() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<span
title="Change Language"
title={t('common:changeLanguage')}
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
>
{LANGUAGES.find(
(lang: { value: string; label: string }) =>
lang.value === currentLanguage
)?.label || 'English'}
)?.label || t('common:english')}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-24">

View File

@ -25,7 +25,7 @@ import {
import { useThreads } from '@/hooks/useThreads'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { useMemo, useState } from 'react'
import {
Dialog,
@ -43,22 +43,22 @@ import { DownloadManagement } from '@/containers/DownloadManegement'
const mainMenus = [
{
title: 'common.newChat',
title: 'common:newChat',
icon: IconCirclePlusFilled,
route: route.home,
},
{
title: 'Assistants',
title: 'common:assistants',
icon: IconClipboardSmileFilled,
route: route.assistant,
},
{
title: 'common.hub',
title: 'common:hub',
icon: IconAppsFilled,
route: route.hub,
},
{
title: 'common.settings',
title: 'common:settings',
icon: IconSettingsFilled,
route: route.settings.general,
},
@ -114,7 +114,7 @@ const LeftPanel = () => {
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
<input
type="text"
placeholder={t('common.search')}
placeholder={t('common:search')}
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
@ -138,7 +138,7 @@ const LeftPanel = () => {
<IconSearch className="absolute size-4 top-1/2 left-2 -translate-y-1/2 text-left-panel-fg/50" />
<input
type="text"
placeholder={t('common.search')}
placeholder={t('common:search')}
className="w-full pl-7 pr-8 py-1 bg-left-panel-fg/10 rounded-sm text-left-panel-fg focus:outline-none focus:ring-1 focus:ring-left-panel-fg/10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
@ -159,7 +159,7 @@ const LeftPanel = () => {
<>
<div className="flex items-center justify-between mb-2">
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold sticky top-0">
{t('common.favorites')}
{t('common:favorites')}
</span>
<div className="relative">
<DropdownMenu>
@ -175,15 +175,14 @@ const LeftPanel = () => {
<DropdownMenuItem
onClick={() => {
unstarAllThreads()
toast.success('All Threads Unfavorited', {
toast.success(t('common:toast.allThreadsUnfavorited.title'), {
id: 'unfav-all-threads',
description:
'All threads have been removed from your favorites.',
description: t('common:toast.allThreadsUnfavorited.description'),
})
}}
>
<IconStar size={16} />
<span>{t('common.unstarAll')}</span>
<span>{t('common:unstarAll')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -206,7 +205,7 @@ const LeftPanel = () => {
{unFavoritedThreads.length > 0 && (
<div className="flex items-center justify-between mb-2">
<span className="block text-xs text-left-panel-fg/50 px-1 font-semibold">
{t('common.recents')}
{t('common:recents')}
</span>
<div className="relative">
<Dialog>
@ -231,15 +230,18 @@ const LeftPanel = () => {
onSelect={(e) => e.preventDefault()}
>
<IconTrash size={16} />
<span>{t('common.deleteAll')}</span>
<span>{t('common:deleteAll')}</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete All Threads</DialogTitle>
<DialogTitle>
{t('common:dialogs.deleteAllThreads.title')}
</DialogTitle>
<DialogDescription>
All threads will be deleted. This action cannot
be undone.
{t(
'common:dialogs.deleteAllThreads.description'
)}
</DialogDescription>
<DialogFooter className="mt-2">
<DialogClose asChild>
@ -248,7 +250,7 @@ const LeftPanel = () => {
size="sm"
className="hover:no-underline"
>
Cancel
{t('common:cancel')}
</Button>
</DialogClose>
<Button
@ -256,17 +258,16 @@ const LeftPanel = () => {
size="sm"
onClick={() => {
deleteAllThreads()
toast.success('Delete All Threads', {
toast.success(t('common:toast.deleteAllThreads.title'), {
id: 'delete-all-thread',
description:
'All threads have been permanently deleted.',
description: t('common:toast.deleteAllThreads.description'),
})
setTimeout(() => {
navigate({ to: route.home })
}, 0)
}}
>
Delete
{t('common:deleteAll')}
</Button>
</DialogFooter>
</DialogHeader>
@ -282,11 +283,12 @@ const LeftPanel = () => {
<div className="px-1 mt-2">
<div className="flex items-center gap-1 text-left-panel-fg/80">
<IconSearch size={18} />
<h6 className="font-medium text-base">No results found</h6>
<h6 className="font-medium text-base">
{t('common:noResultsFound')}
</h6>
</div>
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
We couldn't find any chats matching your search. Try a
different keyword.
{t('common:noResultsFoundDesc')}
</p>
</div>
)}
@ -296,10 +298,12 @@ const LeftPanel = () => {
<div className="px-1 mt-2">
<div className="flex items-center gap-1 text-left-panel-fg/80">
<IconMessageFilled size={18} />
<h6 className="font-medium text-base">No threads yet</h6>
<h6 className="font-medium text-base">
{t('common:noThreadsYet')}
</h6>
</div>
<p className="text-left-panel-fg/60 mt-1 text-xs leading-relaxed">
Start a new conversation to see your thread history here.
{t('common:noThreadsYetDesc')}
</p>
</div>
</>

View File

@ -14,6 +14,7 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { updateModel, stopModel } from '@/services/models'
import { ModelSettingParams } from '@janhq/core'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
type ModelSettingProps = {
provider: ProviderObject
@ -27,6 +28,7 @@ export function ModelSetting({
smallIcon,
}: ModelSettingProps) {
const { updateProvider } = useModelProvider()
const { t } = useTranslation()
// Create a debounced version of stopModel that waits 500ms after the last call
const debouncedStopModel = debounce((modelId: string) => {
@ -104,9 +106,9 @@ export function ModelSetting({
</SheetTrigger>
<SheetContent className="h-[calc(100%-8px)] top-1 right-1 rounded-e-md overflow-y-auto">
<SheetHeader>
<SheetTitle>Model Settings - {model.id}</SheetTitle>
<SheetTitle>{t('common:modelSettings.title', { modelId: model.id })}</SheetTitle>
<SheetDescription>
Configure model settings to optimize performance and behavior.
{t('common:modelSettings.description')}
</SheetDescription>
</SheetHeader>
<div className="px-4 space-y-6">

View File

@ -19,6 +19,7 @@ import { openAIProviderSettings } from '@/mock/data'
import ProvidersAvatar from '@/containers/ProvidersAvatar'
import cloneDeep from 'lodash/cloneDeep'
import { toast } from 'sonner'
import { useTranslation } from '@/i18n/react-i18next-compat'
const ProvidersMenu = ({
stepSetupRemoteProvider,
@ -29,11 +30,11 @@ const ProvidersMenu = ({
const navigate = useNavigate()
const matches = useMatches()
const [name, setName] = useState('')
const { t } = useTranslation()
const createProvider = useCallback(() => {
if (providers.some((e) => e.provider === name)) {
toast.error(
`Provider with name "${name}" already exists. Please choose a different name.`
)
toast.error(t('providerAlreadyExists', { name }))
return
}
const newProvider = {
@ -53,14 +54,14 @@ const ProvidersMenu = ({
},
})
}, 0)
}, [providers, name, addProvider, navigate])
}, [providers, name, addProvider, t, navigate])
return (
<div className="w-44 py-2 border-r border-main-view-fg/5 pb-10 overflow-y-auto">
<Link to={route.settings.general}>
<div className="flex items-center gap-0.5 ml-3 mb-4 mt-1">
<IconArrowLeft size={16} className="text-main-view-fg/70" />
<span className="text-main-view-fg/80">Back</span>
<span className="text-main-view-fg/80">{t('common:back')}</span>
</div>
</Link>
<div className="first-step-setup-remote-provider">
@ -108,17 +109,17 @@ const ProvidersMenu = ({
<DialogTrigger asChild>
<div className="flex cursor-pointer px-4 my-1.5 items-center gap-1.5 text-main-view-fg/80">
<IconCirclePlus size={18} />
<span>Add Provider</span>
<span>{t('provider:addProvider')}</span>
</div>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add OpenAI Provider</DialogTitle>
<DialogTitle>{t('provider:addOpenAIProvider')}</DialogTitle>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-2"
placeholder="Enter a name for your provider"
placeholder={t('provider:enterNameForProvider')}
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
@ -131,12 +132,12 @@ const ProvidersMenu = ({
size="sm"
className="hover:no-underline"
>
Cancel
{t('common:cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button disabled={!name} onClick={createProvider}>
Create
{t('common:create')}
</Button>
</DialogClose>
</DialogFooter>

View File

@ -15,6 +15,7 @@ import { useCodeblock } from '@/hooks/useCodeblock'
import 'katex/dist/katex.min.css'
import { IconCopy, IconCopyCheck } from '@tabler/icons-react'
import rehypeRaw from 'rehype-raw'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface MarkdownProps {
content: string
@ -33,6 +34,7 @@ function RenderMarkdownComponent({
components,
isWrapping,
}: MarkdownProps) {
const { t } = useTranslation()
const { codeBlockStyle, showLineNumbers } = useCodeblock()
// State for tracking which code block has been copied
@ -91,12 +93,12 @@ function RenderMarkdownComponent({
{copiedId === codeId ? (
<>
<IconCopyCheck size={16} className="text-primary" />
<span>Copied!</span>
<span>{t('copied')}</span>
</>
) : (
<>
<IconCopy size={16} />
<span>Copy</span>
<span>{t('copy')}</span>
</>
)}
</button>

View File

@ -1,49 +1,49 @@
import { Link, useMatches } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { useModelProvider } from '@/hooks/useModelProvider'
import { isProd } from '@/lib/version'
const menuSettings = [
{
title: 'common.general',
title: 'common:general',
route: route.settings.general,
},
{
title: 'common.appearance',
title: 'common:appearance',
route: route.settings.appearance,
},
{
title: 'common.privacy',
title: 'common:privacy',
route: route.settings.privacy,
},
{
title: 'common.keyboardShortcuts',
title: 'common:keyboardShortcuts',
route: route.settings.shortcuts,
},
{
title: 'Hardware',
title: 'common:hardware',
route: route.settings.hardware,
},
// Only show MCP Servers in non-production environment
...(!isProd
? [
{
title: 'MCP Servers',
title: 'common:mcp-servers',
route: route.settings.mcp_servers,
},
]
: []),
{
title: 'Local API Server',
title: 'common:local_api_server',
route: route.settings.local_api_server,
},
{
title: 'HTTPS Proxy',
title: 'common:https_proxy',
route: route.settings.https_proxy,
},
{
title: 'Extensions',
title: 'common:extensions',
route: route.settings.extensions,
},
]
@ -83,7 +83,7 @@ const SettingsMenu = () => {
{/* Model Providers Link with default parameter */}
{isActive ? (
<div className="block px-2 mt-1 gap-1.5 py-1 w-full rounded bg-main-view-fg/5 cursor-pointer">
<span>{t('common.modelProviders')}</span>
<span>{t('common:modelProviders')}</span>
</div>
) : (
<Link
@ -93,7 +93,7 @@ const SettingsMenu = () => {
className="block px-2 gap-1.5 cursor-pointer hover:bg-main-view-fg/5 py-1 w-full rounded"
>
<span className="text-main-view-fg/80">
{t('common.modelProviders')}
{t('common:modelProviders')}
</span>
</Link>
)}

View File

@ -4,8 +4,10 @@ import { Link } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import HeaderPage from './HeaderPage'
import { isProd } from '@/lib/version'
import { useTranslation } from '@/i18n/react-i18next-compat'
function SetupScreen() {
const { t } = useTranslation()
const { providers } = useModelProvider()
const firstItemRemoteProvider =
providers.length > 0 ? providers[1].provider : 'openai'
@ -17,11 +19,10 @@ function SetupScreen() {
<div className="w-4/6 mx-auto">
<div className="mb-8 text-left">
<h1 className="font-editorialnew text-main-view-fg text-4xl">
Welcome to Jan
{t('setup:welcome')}
</h1>
<p className="text-main-view-fg/70 text-lg mt-2">
To get started, you'll need to either download a local AI model or
connect to a cloud model using an API key
{t('setup:description')}
</p>
</div>
<div className="flex gap-4 flex-col">
@ -35,7 +36,7 @@ function SetupScreen() {
>
<div>
<h1 className="text-main-view-fg font-medium text-base">
Set up local model
{t('setup:localModel')}
</h1>
</div>
</Link>
@ -53,7 +54,7 @@ function SetupScreen() {
}}
>
<h1 className="text-main-view-fg font-medium text-base">
Set up remote provider
{t('setup:remoteProvider')}
</h1>
</Link>
}

View File

@ -6,12 +6,15 @@ import {
} from '@/components/ui/dropdown-menu'
import { useTheme } from '@/hooks/useTheme'
import { cn } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function ThemeSwitcher() {
const { t } = useTranslation()
const themeOptions = [
{ value: 'dark', label: 'Dark' },
{ value: 'light', label: 'Light' },
{ value: 'auto', label: 'System' },
{ value: 'dark', label: t('common:dark') },
{ value: 'light', label: t('common:light') },
{ value: 'auto', label: t('common:system') },
]
const { setTheme, activeTheme } = useTheme()
@ -20,11 +23,11 @@ export function ThemeSwitcher() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<span
title="Edit Theme"
title={t('common:editTheme')}
className="flex cursor-pointer items-center gap-1 px-2 py-1 rounded-sm bg-main-view-fg/15 text-sm outline-none text-main-view-fg font-medium"
>
{themeOptions.find((item) => item.value === activeTheme)?.label ||
'Auto'}
t('common:auto')}
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-24">

View File

@ -2,6 +2,7 @@ import { ChevronDown, ChevronUp, Loader } from 'lucide-react'
import { create } from 'zustand'
import { RenderMarkdown } from './RenderMarkdown'
import { useAppState } from '@/hooks/useAppState'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface Props {
text: string
@ -28,6 +29,7 @@ const useThinkingStore = create<ThinkingBlockState>((set) => ({
const ThinkingBlock = ({ id, text }: Props) => {
const { thinkingState, toggleState } = useThinkingStore()
const { streamingContent } = useAppState()
const { t } = useTranslation()
const loading = !text.includes('</think>') && streamingContent
const isExpanded = thinkingState[id] ?? false
const handleClick = () => toggleState(id)
@ -51,7 +53,7 @@ const ThinkingBlock = ({ id, text }: Props) => {
<ChevronDown className="size-4 text-main-view-fg/60" />
)}
<span className="font-medium">
{loading ? 'Thinking...' : 'Thought'}
{loading ? t('common:thinking') : t('common:thought')}
</span>
</button>
</div>

View File

@ -39,9 +39,11 @@ import TokenSpeedIndicator from '@/containers/TokenSpeedIndicator'
import CodeEditor from '@uiw/react-textarea-code-editor'
import '@uiw/react-textarea-code-editor/dist.css'
import { useTranslation } from '@/i18n/react-i18next-compat'
const CopyButton = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false)
const { t } = useTranslation()
const handleCopy = () => {
navigator.clipboard.writeText(text)
@ -57,7 +59,7 @@ const CopyButton = ({ text }: { text: string }) => {
{copied ? (
<>
<IconCopyCheck size={16} className="text-accent" />
<span className="opacity-100">Copied!</span>
<span className="opacity-100">{t('copied')}</span>
</>
) : (
<Tooltip>
@ -65,7 +67,7 @@ const CopyButton = ({ text }: { text: string }) => {
<IconCopy size={16} />
</TooltipTrigger>
<TooltipContent>
<p>Copy</p>
<p>{t('copy')}</p>
</TooltipContent>
</Tooltip>
)}
@ -87,6 +89,7 @@ export const ThreadContent = memo(
}
) => {
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
const { t } = useTranslation()
// Use useMemo to stabilize the components prop
const linkComponents = useMemo(
@ -226,13 +229,13 @@ export const ThreadContent = memo(
</div>
</TooltipTrigger>
<TooltipContent>
<p>Edit</p>
<p>{t('edit')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Message</DialogTitle>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
<Textarea
value={message}
onChange={(e) => {
@ -259,10 +262,9 @@ export const ThreadContent = memo(
disabled={!message}
onClick={() => {
editMessage(item.id)
toast.success('Edit Message', {
toast.success(t('common:toast.editMessage.title'), {
id: 'edit-message',
description:
'Message edited successfully. Please wait for the model to respond.',
description: t('common:toast.editMessage.description'),
})
}}
>
@ -285,7 +287,7 @@ export const ThreadContent = memo(
</button>
</TooltipTrigger>
<TooltipContent>
<p>Delete</p>
<p>{t('delete')}</p>
</TooltipContent>
</Tooltip>
</div>
@ -382,7 +384,7 @@ export const ThreadContent = memo(
</button>
</TooltipTrigger>
<TooltipContent>
<p>Delete</p>
<p>{t('delete')}</p>
</TooltipContent>
</Tooltip>
<Dialog>
@ -394,13 +396,13 @@ export const ThreadContent = memo(
</div>
</TooltipTrigger>
<TooltipContent>
<p>Metadata</p>
<p>{t('metadata')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Message Metadata</DialogTitle>
<DialogTitle>{t('common:dialogs.messageMetadata.title')}</DialogTitle>
<div className="space-y-2">
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
<CodeEditor
@ -435,7 +437,7 @@ export const ThreadContent = memo(
</button>
</TooltipTrigger>
<TooltipContent>
<p>Regenerate</p>
<p>{t('regenerate')}</p>
</TooltipContent>
</Tooltip>
)}

View File

@ -30,7 +30,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { DialogClose, DialogFooter, DialogHeader } from '@/components/ui/dialog'
import {
Dialog,
@ -85,7 +85,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
return (thread.title || '').replace(/<span[^>]*>|<\/span>/g, '')
}, [thread.title])
const [title, setTitle] = useState(plainTitleForRename || 'New Thread')
const [title, setTitle] = useState(plainTitleForRename || t('common:newThread'))
return (
<div
@ -101,7 +101,9 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
)}
>
<div className="py-1 pr-2 truncate">
<span>{thread.title || 'New Thread'}</span>
<span
dangerouslySetInnerHTML={{ __html: thread.title || t('common:newThread') }}
/>
</div>
<div className="flex items-center">
<DropdownMenu
@ -127,7 +129,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
}}
>
<IconStarFilled />
<span>{t('common.unstar')}</span>
<span>{t('common:unstar')}</span>
</DropdownMenuItem>
) : (
<DropdownMenuItem
@ -137,26 +139,26 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
}}
>
<IconStar />
<span>{t('common.star')}</span>
<span>{t('common:star')}</span>
</DropdownMenuItem>
)}
<Dialog
onOpenChange={(open) => {
if (!open) {
setOpenDropdown(false)
setTitle(plainTitleForRename || 'New Thread')
setTitle(plainTitleForRename || t('common:newThread'))
}
}}
>
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<IconEdit />
<span>{t('common.rename')}</span>
<span>{t('common:rename')}</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Thread Title</DialogTitle>
<DialogTitle>{t('common:threadTitle')}</DialogTitle>
<Input
value={title}
onChange={(e) => {
@ -175,7 +177,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
size="sm"
className="hover:no-underline"
>
Cancel
{t('common:cancel')}
</Button>
</DialogClose>
<Button
@ -183,14 +185,13 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
onClick={() => {
renameThread(thread.id, title)
setOpenDropdown(false)
toast.success('Rename Thread', {
toast.success(t('common:toast.renameThread.title'), {
id: 'rename-thread',
description:
"Thread title has been renamed to '" + title + "'",
description: t('common:toast.renameThread.description', { title }),
})
}}
>
Rename
{t('common:rename')}
</Button>
</DialogFooter>
</DialogHeader>
@ -206,15 +207,14 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<IconTrash />
<span>{t('common.delete')}</span>
<span>{t('common:delete')}</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Thread</DialogTitle>
<DialogTitle>{t('common:deleteThread')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete this thread? This action
cannot be undone.
{t('common:dialogs.deleteThread.description')}
</DialogDescription>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
@ -223,7 +223,7 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
size="sm"
className="hover:no-underline"
>
Cancel
{t('common:cancel')}
</Button>
</DialogClose>
<Button
@ -231,17 +231,16 @@ const SortableItem = memo(({ thread }: { thread: Thread }) => {
onClick={() => {
deleteThread(thread.id)
setOpenDropdown(false)
toast.success('Delete Thread', {
toast.success(t('common:toast.deleteThread.title'), {
id: 'delete-thread',
description:
'This thread has been permanently deleted.',
description: t('common:toast.deleteThread.description'),
})
setTimeout(() => {
navigate({ to: route.home })
}, 0)
}}
>
Delete
{t('common:delete')}
</Button>
</DialogFooter>
</DialogHeader>

View File

@ -10,6 +10,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { twMerge } from 'tailwind-merge'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface Props {
result: string
@ -127,6 +128,7 @@ const ContentItemRenderer = ({
const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
const { collapseState, setCollapseState } = useToolCallBlockStore()
const { t } = useTranslation()
const isExpanded = collapseState[id] ?? (loading ? true : false)
const [modalImage, setModalImage] = useState<{
url: string
@ -187,7 +189,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
loading ? 'text-main-view-fg/40' : 'text-accent'
)}
>
{loading ? 'Calling tool' : 'Completed'}{' '}
{loading ? t('common:callingTool') : t('common:completed')}{' '}
</span>
</span>
</button>
@ -248,7 +250,7 @@ const ToolCallBlock = ({ id, name, result, loading, args }: Props) => {
>
<DialogContent className="max-w-4xl max-h-[90vh] p-0">
<DialogHeader className="p-6 pb-2">
<DialogTitle>{modalImage?.alt || 'Image'}</DialogTitle>
<DialogTitle>{modalImage?.alt || t('common:image')}</DialogTitle>
</DialogHeader>
<div className="flex justify-center items-center p-6 pt-2">
{modalImage && (

View File

@ -1,10 +1,12 @@
import { Input } from '@/components/ui/input'
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
import { useState, useEffect } from 'react'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function TrustedHostsInput() {
const { trustedHosts, setTrustedHosts } = useLocalApiServer()
const [inputValue, setInputValue] = useState(trustedHosts.join(', '))
const { t } = useTranslation()
// Update input value when trustedHosts changes externally
useEffect(() => {
@ -37,7 +39,7 @@ export function TrustedHostsInput() {
onChange={handleChange}
onBlur={handleBlur}
className="w-full h-8 text-sm"
placeholder="Enter trusted hosts"
placeholder={t('common:enterTrustedHosts')}
/>
)
}

View File

@ -2,8 +2,10 @@ import { Button } from '@/components/ui/button'
import { useAnalytic } from '@/hooks/useAnalytic'
import { IconFileTextShield } from '@tabler/icons-react'
import posthog from 'posthog-js'
import { useTranslation } from '@/i18n/react-i18next-compat'
export function PromptAnalytic() {
const { t } = useTranslation()
const { setProductAnalyticPrompt, setProductAnalytic } = useAnalytic()
const handleProductAnalytics = (isAllowed: boolean) => {
@ -23,7 +25,7 @@ export function PromptAnalytic() {
<div className="flex items-center gap-2">
<IconFileTextShield className="text-accent" />
<h2 className="font-medium text-main-view-fg/80">
Help Us Improve Jan
{t('helpUsImproveJan')}
</h2>
</div>
<p className="mt-2 text-sm text-main-view-fg/70">
@ -41,9 +43,9 @@ export function PromptAnalytic() {
className="text-main-view-fg/70"
onClick={() => handleProductAnalytics(false)}
>
Deny
{t('deny')}
</Button>
<Button onClick={() => handleProductAnalytics(true)}>Allow</Button>
<Button onClick={() => handleProductAnalytics(true)}>{t('allow')}</Button>
</div>
</div>
)

View File

@ -28,7 +28,7 @@ import {
import { useTheme } from '@/hooks/useTheme'
import { teamEmoji } from '@/utils/teamEmoji'
import { AvatarEmoji } from '@/containers/AvatarEmoji'
import { useTranslation } from 'react-i18next'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { cn, isDev } from '@/lib/utils'
interface AddEditAssistantProps {
@ -222,13 +222,17 @@ export default function AddEditAssistant({
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingKey ? 'Edit Assistant' : 'Add Assistant'}
{editingKey
? t('assistants:editAssistant')
: t('assistants:addAssistant')}
</DialogTitle>
</DialogHeader>
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="relative">
<label className="text-sm mb-2 inline-block">Emoji</label>
<label className="text-sm mb-2 inline-block">
{t('assistants:emoji')}
</label>
<div
className="border rounded-sm p-1 w-9 h-9 flex items-center justify-center border-main-view-fg/10 cursor-pointer"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
@ -267,12 +271,12 @@ export default function AddEditAssistant({
<div className="space-y-2 w-full">
<label className="text-sm mb-2 inline-block">
{t(`common.name`)}
{t(`common:name`)}
</label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
placeholder={t('assistants:enterName')}
autoFocus
/>
</div>
@ -280,22 +284,24 @@ export default function AddEditAssistant({
<div className="space-y-2">
<label className="text-sm mb-2 inline-block">
Description (optional)
{t('assistants:description')}
</label>
<Textarea
value={description || ''}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter description"
placeholder={t('assistants:enterDescription')}
className="resize-none"
/>
</div>
<div className="space-y-2">
<label className="text-sm mb-2 inline-block">Instructions</label>
<label className="text-sm mb-2 inline-block">
{t('assistants:instructions')}
</label>
<Textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Enter instructions"
placeholder={t('assistants:enterInstructions')}
className="resize-none"
rows={4}
/>
@ -303,7 +309,9 @@ export default function AddEditAssistant({
<div className="space-y-2 my-4">
<div className="flex items-center justify-between">
<label className="text-sm">Predefined Parameters</label>
<label className="text-sm">
{t('assistants:predefinedParameters')}
</label>
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(paramsSettings).map(([key, setting]) => (
@ -360,7 +368,7 @@ export default function AddEditAssistant({
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm">Parameters</label>
<label className="text-sm">{t('assistants:parameters')}</label>
<div
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
onClick={handleAddParameter}
@ -376,7 +384,7 @@ export default function AddEditAssistant({
onChange={(e) =>
handleParameterChange(index, e.target.value, 'key')
}
placeholder="Key"
placeholder={t('assistants:key')}
className="w-24"
/>
@ -402,28 +410,28 @@ export default function AddEditAssistant({
handleParameterChange(index, 'string', 'type')
}
>
String
{t('assistants:stringValue')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'number', 'type')
}
>
Number
{t('assistants:numberValue')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'boolean', 'type')
}
>
Boolean
{t('assistants:booleanValue')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, 'json', 'type')
}
>
JSON
{t('assistants:jsonValue')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -433,7 +441,11 @@ export default function AddEditAssistant({
<DropdownMenuTrigger asChild>
<div className="relative flex-1">
<Input
value={paramsValues[index] ? 'True' : 'False'}
value={
paramsValues[index]
? t('assistants:trueValue')
: t('assistants:falseValue')
}
readOnly
/>
<IconChevronDown
@ -448,14 +460,14 @@ export default function AddEditAssistant({
handleParameterChange(index, true, 'value')
}
>
True
{t('assistants:trueValue')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
handleParameterChange(index, false, 'value')
}
>
False
{t('assistants:falseValue')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -469,7 +481,7 @@ export default function AddEditAssistant({
onChange={(e) =>
handleParameterChange(index, e.target.value, 'value')
}
placeholder="JSON Value"
placeholder={t('assistants:jsonValuePlaceholder')}
className="flex-1"
/>
) : (
@ -479,7 +491,7 @@ export default function AddEditAssistant({
handleParameterChange(index, e.target.value, 'value')
}
type={paramsTypes[index] === 'number' ? 'number' : 'text'}
placeholder="Value"
placeholder={t('assistants:value')}
className="flex-1"
/>
)}
@ -496,7 +508,7 @@ export default function AddEditAssistant({
</div>
<DialogFooter>
<Button onClick={handleSave}>Save</Button>
<Button onClick={handleSave}>{t('assistants:save')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { IconPlus, IconTrash, IconGripVertical } from '@tabler/icons-react'
import { MCPServerConfig } from '@/hooks/useMCPServers'
import { useTranslation } from '@/i18n/react-i18next-compat'
import {
DndContext,
closestCenter,
@ -107,6 +108,7 @@ export default function AddEditMCPServer({
initialData,
onSave,
}: AddEditMCPServerProps) {
const { t } = useTranslation()
const [serverName, setServerName] = useState('')
const [command, setCommand] = useState('')
const [args, setArgs] = useState<string[]>([''])
@ -230,32 +232,38 @@ export default function AddEditMCPServer({
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingKey ? 'Edit MCP Server' : 'Add MCP Server'}
{editingKey
? t('mcp-servers:editServer')
: t('mcp-servers:addServer')}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label className="text-sm mb-2 inline-block">Server Name</label>
<label className="text-sm mb-2 inline-block">
{t('mcp-servers:serverName')}
</label>
<Input
value={serverName}
onChange={(e) => setServerName(e.target.value)}
placeholder="Enter server name"
placeholder={t('mcp-servers:enterServerName')}
autoFocus
/>
</div>
<div className="space-y-2">
<label className="text-sm mb-2 inline-block">Command</label>
<label className="text-sm mb-2 inline-block">
{t('mcp-servers:command')}
</label>
<Input
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder="Enter command (uvx or npx)"
placeholder={t('mcp-servers:enterCommand')}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm">Arguments</label>
<label className="text-sm">{t('mcp-servers:arguments')}</label>
<div
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
onClick={handleAddArg}
@ -288,7 +296,9 @@ export default function AddEditMCPServer({
onChange={(value) => handleArgChange(index, value)}
onRemove={() => handleRemoveArg(index)}
canRemove={args.length > 1}
placeholder={`Argument ${index + 1}`}
placeholder={t('mcp-servers:argument', {
index: index + 1,
})}
/>
))}
</SortableContext>
@ -297,7 +307,7 @@ export default function AddEditMCPServer({
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm">Environment Variables</label>
<label className="text-sm">{t('mcp-servers:envVars')}</label>
<div
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
onClick={handleAddEnv}
@ -311,13 +321,13 @@ export default function AddEditMCPServer({
<Input
value={key}
onChange={(e) => handleEnvKeyChange(index, e.target.value)}
placeholder="Key"
placeholder={t('mcp-servers:key')}
className="flex-1"
/>
<Input
value={envValues[index] || ''}
onChange={(e) => handleEnvValueChange(index, e.target.value)}
placeholder="Value"
placeholder={t('mcp-servers:value')}
className="flex-1"
/>
{envKeys.length > 1 && (
@ -334,7 +344,7 @@ export default function AddEditMCPServer({
</div>
<DialogFooter>
<Button onClick={handleSave}>Save</Button>
<Button onClick={handleSave}>{t('mcp-servers:save')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -13,6 +13,7 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { IconPlus } from '@tabler/icons-react'
import { useState } from 'react'
import { getProviderTitle } from '@/lib/utils'
import { useTranslation } from '@/i18n/react-i18next-compat'
type DialogAddModelProps = {
provider: ModelProvider
@ -20,6 +21,7 @@ type DialogAddModelProps = {
}
export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
const { t } = useTranslation()
const { updateProvider } = useModelProvider()
const [modelId, setModelId] = useState<string>('')
const [open, setOpen] = useState(false)
@ -62,10 +64,11 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add New Model</DialogTitle>
<DialogTitle>{t('providers:addModel.title')}</DialogTitle>
<DialogDescription>
Add a new model to the {getProviderTitle(provider.provider)}
&nbsp;provider.
{t('providers:addModel.description', {
provider: getProviderTitle(provider.provider),
})}
</DialogDescription>
</DialogHeader>
@ -75,13 +78,14 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
htmlFor="model-id"
className="text-sm font-medium inline-block"
>
Model ID <span className="text-destructive">*</span>
{t('providers:addModel.modelId')}{' '}
<span className="text-destructive">*</span>
</label>
<Input
id="model-id"
value={modelId}
onChange={(e) => setModelId(e.target.value)}
placeholder="Enter model ID"
placeholder={t('providers:addModel.enterModelId')}
required
/>
</div>
@ -96,7 +100,9 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
className="flex items-center gap-1 hover:underline text-primary"
>
<span>
See model list from {getProviderTitle(provider.provider)}
{t('providers:addModel.exploreModels', {
provider: getProviderTitle(provider.provider),
})}
</span>
</a>
</div>
@ -108,7 +114,7 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => {
onClick={handleSubmit}
disabled={!modelId.trim()}
>
Add Model
{t('providers:addModel.addModel')}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -8,8 +8,10 @@ import { useReleaseNotes } from '@/hooks/useReleaseNotes'
import { RenderMarkdown } from '../RenderMarkdown'
import { cn, isDev } from '@/lib/utils'
import { isNightly, isBeta } from '@/lib/version'
import { useTranslation } from '@/i18n/react-i18next-compat'
const DialogAppUpdater = () => {
const { t } = useTranslation()
const {
updateState,
downloadAndInstallUpdate,
@ -67,10 +69,12 @@ const DialogAppUpdater = () => {
/>
<div>
<div className="text-base font-medium">
New Version: Jan {updateState.updateInfo?.version}
{t('updater:newVersion', {
version: updateState.updateInfo?.version,
})}
</div>
<div className="mt-1 text-main-view-fg/70 font-normal mb-2">
There's a new app update available to download.
{t('updater:updateAvailable')}
</div>
</div>
</div>
@ -80,9 +84,7 @@ const DialogAppUpdater = () => {
<div className="max-h-[500px] p-4 w-[400px] overflow-y-scroll text-sm font-normal leading-relaxed">
{isNightly && !isBeta ? (
<p className="text-sm font-normal">
You are using a nightly build. This version is built from
the latest development branch and may not have release
notes.
{t('updater:nightlyBuild')}
</p>
) : (
<RenderMarkdown
@ -111,7 +113,9 @@ const DialogAppUpdater = () => {
className="px-0 text-main-view-fg/70"
onClick={() => setShowReleaseNotes(!showReleaseNotes)}
>
{showReleaseNotes ? 'Hide' : 'Show'} release notes
{showReleaseNotes
? t('updater:hideReleaseNotes')
: t('updater:showReleaseNotes')}
</Button>
<div className="flex gap-x-5">
<Button
@ -119,15 +123,15 @@ const DialogAppUpdater = () => {
className="px-0 text-main-view-fg/70 remind-me-later"
onClick={() => setRemindMeLater(true)}
>
Remind me later
{t('updater:remindMeLater')}
</Button>
<Button
onClick={handleUpdate}
disabled={updateState.isDownloading}
>
{updateState.isDownloading
? 'Downloading...'
: 'Update Now'}
? t('updater:downloading')
: t('updater:updateNow')}
</Button>
</div>
</div>

View File

@ -10,6 +10,7 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { IconFolder } from '@tabler/icons-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface ChangeDataFolderLocationProps {
children: React.ReactNode
@ -28,6 +29,7 @@ export default function ChangeDataFolderLocation({
open,
onOpenChange,
}: ChangeDataFolderLocationProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
@ -35,18 +37,17 @@ export default function ChangeDataFolderLocation({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<IconFolder size={20} />
Change Data Folder Location
{t('settings:dialogs.changeDataFolder.title')}
</DialogTitle>
<DialogDescription>
Are you sure you want to change the data folder location? This will
move all your data to the new location and restart the application.
{t('settings:dialogs.changeDataFolder.description')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
Current Location:
{t('settings:dialogs.changeDataFolder.currentLocation')}
</h4>
<div className="bg-main-view-fg/5 border border-main-view-fg/10 rounded">
<code className="text-xs text-main-view-fg/70 break-all">
@ -57,7 +58,7 @@ export default function ChangeDataFolderLocation({
<div>
<h4 className="text-sm font-medium text-main-view-fg/80 mb-2">
New Location:
{t('settings:dialogs.changeDataFolder.newLocation')}
</h4>
<div className="bg-accent/10 border border-accent/20 rounded">
<code className="text-xs text-accent break-all">{newPath}</code>
@ -68,11 +69,13 @@ export default function ChangeDataFolderLocation({
<DialogFooter className="flex items-center gap-2">
<DialogClose asChild>
<Button variant="link" size="sm">
Cancel
{t('settings:dialogs.changeDataFolder.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button onClick={onConfirm}>Change Location</Button>
<Button onClick={onConfirm}>
{t('settings:dialogs.changeDataFolder.changeLocation')}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>

View File

@ -7,6 +7,7 @@ import {
DialogDescription,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface DeleteMCPServerConfirmProps {
open: boolean
@ -21,15 +22,15 @@ export default function DeleteMCPServerConfirm({
serverName,
onConfirm,
}: DeleteMCPServerConfirmProps) {
const { t } = useTranslation()
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete MCP Server</DialogTitle>
<DialogTitle>{t('mcp-servers:deleteServer.title')}</DialogTitle>
<DialogDescription>
Are you sure you want to delete the MCP server{' '}
{t('mcp-servers:deleteServer.description', { serverName })}
<span className="font-medium text-main-view-fg">{serverName}</span>?
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
@ -40,7 +41,7 @@ export default function DeleteMCPServerConfirm({
onOpenChange(false)
}}
>
Delete
{t('mcp-servers:deleteServer.delete')}
</Button>
</DialogFooter>
</DialogContent>

View File

@ -17,6 +17,7 @@ import { IconTrash } from '@tabler/icons-react'
import { useState, useEffect } from 'react'
import { toast } from 'sonner'
import { useTranslation } from '@/i18n/react-i18next-compat'
type DialogDeleteModelProps = {
provider: ModelProvider
@ -27,6 +28,7 @@ export const DialogDeleteModel = ({
provider,
modelId,
}: DialogDeleteModelProps) => {
const { t } = useTranslation()
const [selectedModelId, setSelectedModelId] = useState<string>('')
const { setProviders, deleteModel: deleteModelCache } = useModelProvider()
@ -34,10 +36,15 @@ export const DialogDeleteModel = ({
deleteModelCache(selectedModelId)
deleteModel(selectedModelId).then(() => {
getProviders().then(setProviders)
toast.success('Delete Model', {
id: `delete-model-${selectedModel?.id}`,
description: `Model ${selectedModel?.id} has been permanently deleted.`,
})
toast.success(
t('providers:deleteModel.title', { modelId: selectedModel?.id }),
{
id: `delete-model-${selectedModel?.id}`,
description: t('providers:deleteModel.success', {
modelId: selectedModel?.id,
}),
}
)
})
}
@ -69,22 +76,23 @@ export const DialogDeleteModel = ({
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Model: {selectedModel.id}</DialogTitle>
<DialogTitle>
{t('providers:deleteModel.title', { modelId: selectedModel.id })}
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this model? This action cannot be
undone.
{t('providers:deleteModel.description')}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-2">
<DialogClose asChild>
<Button variant="link" size="sm" className="hover:no-underline">
Cancel
{t('providers:deleteModel.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button variant="destructive" size="sm" onClick={removeModel}>
Delete
{t('providers:deleteModel.delete')}
</Button>
</DialogClose>
</DialogFooter>

View File

@ -18,11 +18,13 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { useRouter } from '@tanstack/react-router'
import { route } from '@/constants/routes'
import { normalizeProvider } from '@/lib/models'
import { useTranslation } from '@/i18n/react-i18next-compat'
type Props = {
provider?: ProviderObject
}
const DeleteProvider = ({ provider }: Props) => {
const { t } = useTranslation()
const { deleteProvider, providers } = useModelProvider()
const router = useRouter()
if (
@ -34,9 +36,11 @@ const DeleteProvider = ({ provider }: Props) => {
const removeProvider = async () => {
deleteProvider(provider.provider)
toast.success('Delete Provider', {
toast.success(t('providers:deleteProvider.title'), {
id: `delete-provider-${provider.provider}`,
description: `Provider ${provider.provider} has been permanently deleted.`,
description: t('providers:deleteProvider.success', {
provider: provider.provider,
}),
})
setTimeout(() => {
router.navigate({
@ -50,28 +54,31 @@ const DeleteProvider = ({ provider }: Props) => {
return (
<CardItem
title="Delete Provider"
description="Delete this provider and all its models. This action cannot be undone."
title={t('providers:deleteProvider.title')}
description={t('providers:deleteProvider.description')}
actions={
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" size="sm">
Delete
{t('providers:deleteProvider.delete')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Provider: {provider.provider}</DialogTitle>
<DialogTitle>
{t('providers:deleteProvider.confirmTitle', {
provider: provider.provider,
})}
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this provider? This action
cannot be undone.
{t('providers:deleteProvider.confirmDescription')}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-2">
<DialogClose asChild>
<Button variant="link" size="sm" className="hover:no-underline">
Cancel
{t('providers:deleteProvider.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
@ -80,7 +87,7 @@ const DeleteProvider = ({ provider }: Props) => {
size="sm"
onClick={removeProvider}
>
Delete
{t('providers:deleteProvider.delete')}
</Button>
</DialogClose>
</DialogFooter>

View File

@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'
import { MCPServerConfig } from '@/hooks/useMCPServers'
import CodeEditor from '@uiw/react-textarea-code-editor'
import '@uiw/react-textarea-code-editor/dist.css'
import { useTranslation } from '@/i18n/react-i18next-compat'
interface EditJsonMCPserverProps {
open: boolean
@ -26,6 +27,7 @@ export default function EditJsonMCPserver({
initialData,
onSave,
}: EditJsonMCPserverProps) {
const { t } = useTranslation()
const [jsonContent, setJsonContent] = useState('')
const [error, setError] = useState<string | null>(null)
@ -36,10 +38,10 @@ export default function EditJsonMCPserver({
setJsonContent(JSON.stringify(initialData, null, 2))
setError(null)
} catch {
setError('Failed to parse initial data')
setError(t('mcp-servers:editJson.errorParse'))
}
}
}, [open, initialData])
}, [open, initialData, t])
const handlePaste = (e: React.ClipboardEvent) => {
const pastedText = e.clipboardData.getData('text')
@ -51,7 +53,7 @@ export default function EditJsonMCPserver({
setError(null)
} catch (error) {
e.preventDefault()
setError('Invalid JSON format in pasted content')
setError(t('mcp-servers:editJson.errorPaste'))
console.error('Paste error:', error)
}
}
@ -63,7 +65,7 @@ export default function EditJsonMCPserver({
onOpenChange(false)
setError(null)
} catch {
setError('Invalid JSON format')
setError(t('mcp-servers:editJson.errorFormat'))
}
}
@ -73,8 +75,8 @@ export default function EditJsonMCPserver({
<DialogHeader>
<DialogTitle>
{serverName
? `Edit JSON for MCP Server: ${serverName}`
: 'Edit All MCP Servers JSON'}
? t('mcp-servers:editJson.title', { serverName })
: t('mcp-servers:editJson.titleAll')}
</DialogTitle>
</DialogHeader>
<div className="space-y-2">
@ -82,7 +84,7 @@ export default function EditJsonMCPserver({
<CodeEditor
value={jsonContent}
language="json"
placeholder="Enter JSON configuration"
placeholder={t('mcp-servers:editJson.placeholder')}
onChange={(e) => setJsonContent(e.target.value)}
onPaste={handlePaste}
style={{
@ -96,7 +98,7 @@ export default function EditJsonMCPserver({
</div>
<DialogFooter>
<Button onClick={handleSave}>Save</Button>
<Button onClick={handleSave}>{t('mcp-servers:editJson.save')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -22,6 +22,7 @@ import {
IconCodeCircle2,
} from '@tabler/icons-react'
import { useState, useEffect } from 'react'
import { useTranslation } from '@/i18n/react-i18next-compat'
// No need to define our own interface, we'll use the existing Model type
type DialogEditModelProps = {
@ -33,6 +34,7 @@ export const DialogEditModel = ({
provider,
modelId,
}: DialogEditModelProps) => {
const { t } = useTranslation()
const { updateProvider } = useModelProvider()
const [selectedModelId, setSelectedModelId] = useState<string>('')
const [capabilities, setCapabilities] = useState<Record<string, boolean>>({
@ -140,20 +142,24 @@ export const DialogEditModel = ({
<DialogContent>
<DialogHeader>
<DialogTitle className="line-clamp-1" title={selectedModel.id}>
Edit Model: {selectedModel.id}
{t('providers:editModel.title', { modelId: selectedModel.id })}
</DialogTitle>
<DialogDescription>
Configure model capabilities by toggling the options below.
{t('providers:editModel.description')}
</DialogDescription>
</DialogHeader>
<div className="py-1">
<h3 className="text-sm font-medium mb-3">Capabilities</h3>
<h3 className="text-sm font-medium mb-3">
{t('providers:editModel.capabilities')}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconTool className="size-4 text-main-view-fg/70" />
<span className="text-sm">Tools</span>
<span className="text-sm">
{t('providers:editModel.tools')}
</span>
</div>
<Switch
id="tools-capability"
@ -167,7 +173,9 @@ export const DialogEditModel = ({
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconEye className="size-4 text-main-view-fg/70" />
<span className="text-sm">Vision</span>
<span className="text-sm">
{t('providers:editModel.vision')}
</span>
</div>
<Tooltip>
<TooltipTrigger>
@ -180,14 +188,18 @@ export const DialogEditModel = ({
}
/>
</TooltipTrigger>
<TooltipContent>Not available yet</TooltipContent>
<TooltipContent>
{t('providers:editModel.notAvailable')}
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconCodeCircle2 className="size-4 text-main-view-fg/70" />
<span className="text-sm">Embeddings</span>
<span className="text-sm">
{t('providers:editModel.embeddings')}
</span>
</div>
<Tooltip>
<TooltipTrigger>
@ -200,7 +212,9 @@ export const DialogEditModel = ({
}
/>
</TooltipTrigger>
<TooltipContent>Not available yet</TooltipContent>
<TooltipContent>
{t('providers:editModel.notAvailable')}
</TooltipContent>
</Tooltip>
</div>
@ -221,7 +235,7 @@ export const DialogEditModel = ({
{/* <div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<IconAtom className="size-4 text-main-view-fg/70" />
<span className="text-sm">Reasoning</span>
<span className="text-sm">{t('reasoning')}</span>
</div>
<Switch
id="reasoning-capability"

View File

@ -9,8 +9,11 @@ import {
import { Button } from '@/components/ui/button'
import { useToolApproval } from '@/hooks/useToolApproval'
import { AlertTriangle } from 'lucide-react'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { Trans } from 'react-i18next'
export default function ToolApproval() {
const { t } = useTranslation()
const { isModalOpen, modalProps, setModalOpen } = useToolApproval()
if (!modalProps) {
@ -47,9 +50,13 @@ export default function ToolApproval() {
<AlertTriangle className="size-4" />
</div>
<div>
<DialogTitle>Tool Call Request</DialogTitle>
<DialogTitle>{t('tools:toolApproval.title')}</DialogTitle>
<DialogDescription className="mt-1 text-main-view-fg/70">
The assistant wants to use the tool: <strong>{toolName}</strong>
<Trans
i18nKey="tools:toolApproval.description"
values={{ toolName }}
components={{ strong: <strong className="font-semibold" /> }}
/>
</DialogDescription>
</div>
</div>
@ -57,26 +64,30 @@ export default function ToolApproval() {
<div className="bg-main-view-fg/8 p-2 border border-main-view-fg/5 rounded-lg">
<p className="text-sm text-main-view-fg/70 leading-relaxed">
<strong>Security Notice:</strong> Malicious tools or conversation
content could potentially trick the assistant into attempting
harmful actions. Review each tool call carefully before approving.
{t('tools:toolApproval.securityNotice')}
</p>
</div>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
<Button variant="link" onClick={handleDeny} className="w-full">
Deny
</Button>
<DialogFooter className="flex flex-col gap-2 sm:flex-row sm:justify-between">
<Button
variant="link"
onClick={handleAllowOnce}
className="border border-main-view-fg/20"
onClick={handleDeny}
className="flex-1 text-right sm:flex-none"
>
Allow Once
</Button>
<Button variant="default" onClick={handleAllow}>
Always Allow
{t('tools:toolApproval.deny')}
</Button>
<div className="flex flex-col sm:flex-row sm:gap-2 sm:items-center">
<Button
variant="link"
onClick={handleAllowOnce}
className="border border-main-view-fg/20"
>
{t('tools:toolApproval.allowOnce')}
</Button>
<Button variant="default" onClick={handleAllow}>
{t('tools:toolApproval.alwaysAllow')}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@ -1,47 +1,7 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
// Re-export our custom i18n implementation
export { default } from '@/i18n/setup'
import enCommon from '@/locales/en/common.json'
import idCommon from '@/locales/id/common.json'
import vnCommon from '@/locales/vn/common.json'
import enChat from '@/locales/en/chat.json'
import idChat from '@/locales/id/chat.json'
import vnChat from '@/locales/vn/chat.json'
import enSettings from '@/locales/en/settings.json'
import idSettings from '@/locales/id/settings.json'
import vnSettings from '@/locales/vn/settings.json'
import { localStorageKey } from '@/constants/localStorage'
const stored = localStorage.getItem(localStorageKey.settingGeneral)
const parsed = stored ? JSON.parse(stored) : {}
const defaultLang = parsed?.state?.currentLanguage
i18n.use(initReactI18next).init({
resources: {
en: {
chat: enChat,
common: enCommon,
settings: enSettings,
},
id: {
chat: idChat,
common: idCommon,
settings: idSettings,
},
vn: {
chat: vnChat,
common: vnCommon,
settings: vnSettings,
},
},
lng: defaultLang,
fallbackLng: 'en',
ns: ['chat', 'common', 'settings'],
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
})
export default i18n
// Re-export compatibility functions for existing code
export { useTranslation } from '@/i18n/react-i18next-compat'
export { useAppTranslation } from '@/i18n/hooks'
export { TranslationProvider } from '@/i18n/TranslationContext'

View File

@ -0,0 +1,46 @@
import React, { ReactNode, useEffect, useCallback } from "react"
import i18next, { loadTranslations } from "./setup"
import { useGeneralSetting } from "@/hooks/useGeneralSetting"
import { TranslationContext } from "./context"
// Translation provider component
export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// Get the current language from general settings
const { currentLanguage } = useGeneralSetting()
// Load translations once when the component mounts
useEffect(() => {
try {
loadTranslations()
} catch (error) {
console.error("Failed to load translations:", error)
}
}, [])
// Update language when currentLanguage changes
useEffect(() => {
if (currentLanguage) {
i18next.changeLanguage(currentLanguage)
}
}, [currentLanguage])
// Memoize the translation function to prevent unnecessary re-renders
const translate = useCallback(
(key: string, options?: Record<string, unknown>) => {
return i18next.t(key, options)
},
[],
)
return (
<TranslationContext.Provider
value={{
t: translate,
i18n: i18next,
}}>
{children}
</TranslationContext.Provider>
)
}
export default TranslationProvider

View File

@ -0,0 +1,11 @@
import { createContext } from "react"
import i18next from "./setup"
// Create context for translations
export const TranslationContext = createContext<{
t: (key: string, options?: Record<string, unknown>) => string
i18n: typeof i18next
}>({
t: (key: string) => key,
i18n: i18next,
})

View File

@ -0,0 +1,5 @@
import { useContext } from "react"
import { TranslationContext } from "./context"
// Custom hook for easy translations
export const useAppTranslation = () => useContext(TranslationContext)

View File

@ -0,0 +1,8 @@
// Export the main i18n setup
export { default as i18n, loadTranslations } from './setup'
// Export the React context and hook
export { TranslationProvider, useAppTranslation, TranslationContext } from './TranslationContext'
// Export types
export type { I18nInstance, TranslationResources } from './setup'

View File

@ -0,0 +1,34 @@
import { useAppTranslation } from './hooks'
// Compatibility layer for react-i18next
// This allows existing code to work without changes
/**
* Hook that mimics react-i18next's useTranslation hook
* @param namespace - Optional namespace (not used in our implementation as we handle it in the key)
* @returns Object with t function and i18n instance
*/
export const useTranslation = (namespace?: string) => {
const { t, i18n: i18nInstance } = useAppTranslation()
// If namespace is provided, we can prefix keys with it
const namespacedT = namespace
? (key: string, options?: Record<string, unknown>) => {
// If key already has namespace, use as-is, otherwise prefix with namespace
const finalKey = key.includes(':') ? key : `${namespace}:${key}`
return t(finalKey, options)
}
: t
return {
t: namespacedT,
i18n: i18nInstance,
}
}
// Export the i18n instance for direct usage
export { default as i18n } from './setup'
// Re-export other utilities
export { TranslationProvider } from './TranslationContext'
export { useAppTranslation } from './hooks'

156
web-app/src/i18n/setup.ts Normal file
View File

@ -0,0 +1,156 @@
import { localStorageKey } from '@/constants/localStorage'
// Types for our i18n implementation
export interface TranslationResources {
[language: string]: {
[namespace: string]: {
[key: string]: string
}
}
}
export interface I18nInstance {
language: string
fallbackLng: string
resources: TranslationResources
namespaces: string[]
defaultNS: string
changeLanguage: (lng: string) => void
t: (key: string, options?: Record<string, unknown>) => string
}
// Global i18n instance
let i18nInstance: I18nInstance
// Dynamically load locale files
const localeFiles = import.meta.glob('../locales/**/*.json', { eager: true })
const resources: TranslationResources = {}
const namespaces: string[] = []
// Process all locale files
Object.entries(localeFiles).forEach(([path, module]) => {
// Example path: '../locales/en/common.json' -> language: 'en', namespace: 'common'
const match = path.match(/\.\.\/locales\/([^/]+)\/([^/]+)\.json/)
if (match) {
const [, language, namespace] = match
// Initialize language object if it doesn't exist
if (!resources[language]) {
resources[language] = {}
}
// Add namespace to list if it's not already there
if (!namespaces.includes(namespace)) {
namespaces.push(namespace)
}
// Add namespace resources to language
resources[language][namespace] = (module as { default: { [key: string]: string } }).default || (module as { [key: string]: string })
}
})
// Get stored language preference
const getStoredLanguage = (): string => {
try {
const stored = localStorage.getItem(localStorageKey.settingGeneral)
const parsed = stored ? JSON.parse(stored) : {}
return parsed?.state?.currentLanguage || 'en'
} catch {
return 'en'
}
}
// Translation function
const translate = (key: string, options: Record<string, unknown> = {}): string => {
const { language, fallbackLng, resources: res, defaultNS } = i18nInstance
// Parse key to extract namespace and actual key
let namespace = defaultNS
let translationKey = key
if (key.includes(':')) {
const parts = key.split(':')
namespace = parts[0]
translationKey = parts[1]
}
// Helper function to get nested value from object using dot notation
const getNestedValue = (obj: Record<string, unknown>, path: string): string | undefined => {
return path.split('.').reduce((current, key) => {
return current && typeof current === 'object' && current !== null && key in current
? (current as Record<string, unknown>)[key]
: undefined
}, obj as unknown)
}
// Try to get translation from current language
let translation = getNestedValue(res[language]?.[namespace], translationKey)
// Fallback to fallback language if not found
if (translation === undefined && language !== fallbackLng) {
translation = getNestedValue(res[fallbackLng]?.[namespace], translationKey)
}
// If still not found, return the key itself
if (translation === undefined) {
console.warn(`Translation missing for key: ${key}`)
return key
}
// Handle interpolation
if (typeof translation === 'string' && options) {
return translation.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
return options[variable] !== undefined ? String(options[variable]) : match
})
}
return String(translation)
}
// Change language function
const changeLanguage = (lng: string): void => {
if (i18nInstance && resources[lng]) {
i18nInstance.language = lng
// Update localStorage
try {
const stored = localStorage.getItem(localStorageKey.settingGeneral)
const parsed = stored ? JSON.parse(stored) : { state: {} }
parsed.state.currentLanguage = lng
localStorage.setItem(localStorageKey.settingGeneral, JSON.stringify(parsed))
} catch (error) {
console.error('Failed to save language preference:', error)
}
}
}
// Initialize i18n instance
const initI18n = (): I18nInstance => {
const currentLanguage = getStoredLanguage()
i18nInstance = {
language: currentLanguage,
fallbackLng: 'en',
resources,
namespaces,
defaultNS: 'common',
changeLanguage,
t: translate,
}
return i18nInstance
}
// Load translations function (for compatibility with reference implementation)
export const loadTranslations = (): void => {
// Translations are already loaded via import.meta.glob
// This function exists for compatibility but doesn't need to do anything
console.log('Translations loaded:', Object.keys(resources))
}
// Initialize and export the i18n instance
const i18n = initI18n()
export default i18n

View File

@ -0,0 +1,32 @@
{
"title": "Assistants",
"editAssistant": "Edit Assistant",
"deleteAssistant": "Delete Assistant",
"deleteConfirmation": "Delete Assistant",
"deleteConfirmationDesc": "Are you sure you want to delete this assistant? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete",
"addAssistant": "Add Assistant",
"emoji": "Emoji",
"name": "Name",
"enterName": "Enter name",
"description": "Description (optional)",
"enterDescription": "Enter description",
"instructions": "Instructions",
"enterInstructions": "Enter instructions",
"predefinedParameters": "Predefined Parameters",
"parameters": "Parameters",
"key": "Key",
"value": "Value",
"stringValue": "String",
"numberValue": "Number",
"booleanValue": "Boolean",
"jsonValue": "JSON",
"trueValue": "True",
"falseValue": "False",
"jsonValuePlaceholder": "JSON Value",
"save": "Save",
"createNew": "Create New Assistant",
"personality": "Personality",
"capabilities": "Capabilities"
}

View File

@ -1,9 +1,10 @@
{
"chat": {
"welcome": "Hi, how are you?",
"description": "How can I help you today?",
"status": {
"empty": "No Chats Found"
}
}
}
"welcome": "Hi, how are you?",
"description": "How can I help you today?",
"status": {
"empty": "No Chats Found"
},
"sendMessage": "Send Message",
"newConversation": "New Conversation",
"clearHistory": "Clear History"
}

View File

@ -1,32 +1,267 @@
{
"common": {
"general": "General",
"settings": "Settings",
"modelProviders": "Model Providers",
"appearance": "Appearance",
"privacy": "Privacy",
"keyboardShortcuts": "Shortcuts",
"newChat": "New Chat",
"favorites": "Favorites",
"recents": "Recents",
"hub": "Hub",
"helpSupport": "Help & Support",
"unstarAll": "Unstar All",
"unstar": "Unstar",
"deleteAll": "Delete All",
"star": "Star",
"rename": "Rename",
"delete": "Delete",
"dataFolder": "Data Folder",
"others": "Other",
"language": "Language",
"reset": "Reset",
"search": "Search",
"name": "Name",
"assistants": "Assistants",
"hardware": "Hardware",
"mcp-servers": "Mcp Servers",
"local_api_server": "Local API Server",
"https_proxy": "HTTPS Proxy",
"extensions": "Extensions",
"general": "General",
"settings": "Settings",
"modelProviders": "Model Providers",
"appearance": "Appearance",
"privacy": "Privacy",
"keyboardShortcuts": "Shortcuts",
"newChat": "New Chat",
"favorites": "Favorites",
"recents": "Recents",
"hub": "Hub",
"helpSupport": "Help & Support",
"helpUsImproveJan": "Help Us Improve Jan",
"unstarAll": "Unstar All",
"unstar": "Unstar",
"deleteAll": "Delete All",
"star": "Star",
"rename": "Rename",
"delete": "Delete",
"copied": "Copied!",
"dataFolder": "Data Folder",
"others": "Other",
"language": "Language",
"reset": "Reset",
"search": "Search",
"name": "Name",
"cancel": "Cancel",
"create": "Create",
"save": "Save",
"edit": "Edit",
"copy": "Copy",
"back": "Back",
"close": "Close",
"next": "Next",
"finish": "Finish",
"skip": "Skip",
"allow": "Allow",
"deny": "Deny",
"start": "Start",
"stop": "Stop",
"preview": "Preview",
"compactWidth": "Compact Width",
"fullWidth": "Full Width",
"dark": "Dark",
"light": "Light",
"system": "System",
"auto": "Auto",
"english": "English",
"medium": "Medium",
"newThread": "New Thread",
"noResultsFound": "No results found",
"noThreadsYet": "No threads yet",
"noThreadsYetDesc": "Start a new conversation to see your thread history here.",
"downloads": "Downloads",
"downloading": "Downloading",
"cancelDownload": "Cancel download",
"downloadCancelled": "Download Cancelled",
"downloadComplete": "Download Complete",
"thinking": "Thinking...",
"thought": "Thought",
"callingTool": "Calling tool",
"completed": "Completed",
"image": "Image",
"vision": "Vision",
"embeddings": "Embeddings",
"tools": "Tools",
"webSearch": "Web Search",
"reasoning": "Reasoning",
"selectAModel": "Select a model",
"noToolsAvailable": "No tools available",
"noModelsFoundFor": "No models found for \"{{searchValue}}\"",
"customAvatar": "Custom avatar",
"editAssistant": "Edit Assistant",
"jan": "Jan",
"metadata": "Metadata",
"regenerate": "Regenerate",
"threadImage": "Thread image",
"editMessage": "Edit Message",
"deleteMessage": "Delete Message",
"deleteThread": "Delete Thread",
"renameThread": "Rename Thread",
"threadTitle": "Thread Title",
"deleteAllThreads": "Delete All Threads",
"allThreadsUnfavorited": "All Threads Unfavorited",
"deleteAllThreadsConfirm": "Are you sure you want to delete all threads? This action cannot be undone.",
"addProvider": "Add Provider",
"addOpenAIProvider": "Add OpenAI Provider",
"enterNameForProvider": "Enter a name for your provider",
"providerAlreadyExists": "Provider with name \"{{name}}\" already exists. Please choose a different name.",
"adjustFontSize": "Adjust Font Size",
"changeLanguage": "Change Language",
"editTheme": "Edit Theme",
"editCodeBlockStyle": "Edit Code Block Style",
"editServerHost": "Edit Server Host",
"pickColorWindowBackground": "Pick Color Window Background",
"pickColorAppMainView": "Pick Color App Main View",
"pickColorAppPrimary": "Pick Color App Primary",
"pickColorAppAccent": "Pick Color App Accent",
"pickColorAppDestructive": "Pick Color App Destructive",
"apiKeyRequired": "API Key is required",
"enterTrustedHosts": "Enter trusted hosts",
"placeholder": {
"chatInput": "Ask me anything..."
},
"confirm": "Confirm",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"warning": "Warning",
"noResultsFoundDesc": "We couldn't find any chats matching your search. Try a different keyword.",
"searchModels": "Search models...",
"searchStyles": "Search styles...",
"createAssistant": "Create Assistant",
"enterApiKey": "Enter API Key",
"scrollToBottom": "Scroll to bottom",
"addModel": {
"title": "Add Model",
"modelId": "Model ID",
"enterModelId": "Enter Model ID",
"addModel": "Add Model",
"description": "Add a new model to the provider",
"exploreModels": "See model list from provider"
},
"mcpServers": {
"editServer": "Edit Server",
"addServer": "Add Server",
"serverName": "Server Name",
"enterServerName": "Enter server name",
"command": "Command",
"enterCommand": "Enter command",
"arguments": "Arguments",
"argument": "Argument {{index}}",
"envVars": "Environment Variables",
"key": "Key",
"value": "Value",
"save": "Save"
},
"deleteServer": {
"title": "Delete Server",
"delete": "Delete"
},
"editJson": {
"errorParse": "Failed to parse JSON",
"errorPaste": "Failed to paste JSON",
"errorFormat": "Invalid JSON format",
"titleAll": "Edit All Servers Configuration",
"placeholder": "Enter JSON configuration...",
"save": "Save"
},
"editModel": {
"title": "Edit Model: {{modelId}}",
"description": "Configure model capabilities by toggling the options below.",
"capabilities": "Capabilities",
"tools": "Tools",
"vision": "Vision",
"embeddings": "Embeddings",
"notAvailable": "Not available yet"
},
"outOfContextError": {
"truncateInput": "Truncate Input",
"title": "Out of context error",
"description": "This chat is reaching the AI's 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 computer's 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?",
"increaseContextSize": "Increase Context Size"
},
"toolApproval": {
"title": "Tool Permission Request",
"description": "The assistant wants to use <strong>{{toolName}}</strong>",
"securityNotice": "Only allow tools you trust. Tools can access your system and data.",
"deny": "Deny",
"allowOnce": "Allow Once",
"alwaysAllow": "Always Allow"
},
"deleteModel": {
"title": "Delete Model: {{modelId}}",
"description": "Are you sure you want to delete this model? This action cannot be undone.",
"success": "Model {{modelId}} has been permanently deleted.",
"cancel": "Cancel",
"placeholder": {
"chatInput": "Ask me anything..."
"delete": "Delete"
},
"deleteProvider": {
"title": "Delete Provider",
"description": "Delete this provider and all its models. This action cannot be undone.",
"success": "Provider {{provider}} has been permanently deleted.",
"confirmTitle": "Delete Provider: {{provider}}",
"confirmDescription": "Are you sure you want to delete this provider? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete"
},
"modelSettings": {
"title": "Model Settings - {{modelId}}",
"description": "Configure model settings to optimize performance and behavior."
},
"dialogs": {
"changeDataFolder": {
"title": "Change Data Folder Location",
"description": "Are you sure you want to change the data folder location? This will move all your data to the new location and restart the application.",
"currentLocation": "Current Location:",
"newLocation": "New Location:",
"cancel": "Cancel",
"changeLocation": "Change Location"
},
"deleteAllThreads": {
"title": "Delete All Threads",
"description": "All threads will be deleted. This action cannot be undone."
},
"deleteThread": {
"description": "Are you sure you want to delete this thread? This action cannot be undone."
},
"editMessage": {
"title": "Edit Message"
},
"messageMetadata": {
"title": "Message Metadata"
}
},
"toast": {
"allThreadsUnfavorited": {
"title": "All Threads Unfavorited",
"description": "All threads have been removed from your favorites."
},
"deleteAllThreads": {
"title": "Delete All Threads",
"description": "All threads have been permanently deleted."
},
"renameThread": {
"title": "Rename Thread",
"description": "Thread title has been renamed to '{{title}}'"
},
"deleteThread": {
"title": "Delete Thread",
"description": "This thread has been permanently deleted."
},
"editMessage": {
"title": "Edit Message",
"description": "Message edited successfully. Please wait for the model to respond."
},
"appUpdateDownloaded": {
"title": "App Update Downloaded",
"description": "The app update has been downloaded successfully."
},
"appUpdateDownloadFailed": {
"title": "App Update Download Failed",
"description": "Failed to download the app update. Please try again."
},
"downloadComplete": {
"title": "Download Complete",
"description": "The model {{modelId}} has been downloaded"
},
"downloadCancelled": {
"title": "Download Cancelled",
"description": "The download process was cancelled"
}
},
"cortexFailureDialog": {
"title": "Local AI Engine Issue",
"description": "The local AI engine (Cortex) failed to start after multiple attempts. This might prevent some features from working correctly.",
"contactSupport": "Contact Support",
"restartJan": "Restart Jan"
}
}

View File

@ -0,0 +1,31 @@
{
"sortNewest": "Newest",
"sortMostDownloaded": "Most downloaded",
"use": "Use",
"download": "Download",
"downloaded": "Downloaded",
"loadingModels": "Loading models...",
"noModels": "No models found",
"by": "By",
"downloads": "Downloads",
"variants": "Variants",
"showVariants": "Show variants",
"useModel": "Use this model",
"downloadModel": "Download model",
"searchPlaceholder": "Search for models on Hugging Face...",
"editTheme": "Edit Theme",
"joyride": {
"recommendedModelTitle": "Recommended Model",
"recommendedModelContent": "Browse and download powerful AI models from various providers, all in one place. We suggest starting with Jan-Nano - a model optimized for function calling, tool integration, and research capabilities. It's ideal for building interactive AI agents.",
"downloadInProgressTitle": "Download in Progress",
"downloadInProgressContent": "Your model is now downloading. Track progress here - once finished, it will be ready to use.",
"downloadModelTitle": "Download Model",
"downloadModelContent": "Click the Download button to begin downloading the model.",
"back": "Back",
"close": "Close",
"lastWithDownload": "Download",
"last": "Finish",
"next": "Next",
"skip": "Skip"
}
}

View File

@ -0,0 +1,3 @@
{
"noLogs": "No logs available"
}

View File

@ -0,0 +1,43 @@
{
"editServer": "Edit MCP Server",
"addServer": "Add MCP Server",
"serverName": "Server Name",
"enterServerName": "Enter server name",
"command": "Command",
"enterCommand": "Enter command (uvx or npx)",
"arguments": "Arguments",
"argument": "Argument {{index}}",
"envVars": "Environment Variables",
"key": "Key",
"value": "Value",
"save": "Save",
"status": "Status",
"connected": "Connected",
"disconnected": "Disconnected",
"deleteServer": {
"title": "Delete MCP Server",
"description": "Are you sure you want to delete the MCP server {{serverName}}? This action cannot be undone.",
"delete": "Delete"
},
"editJson": {
"title": "Edit JSON for MCP Server: {{serverName}}",
"titleAll": "Edit All MCP Servers JSON",
"placeholder": "Enter JSON configuration",
"errorParse": "Failed to parse initial data",
"errorPaste": "Invalid JSON format in pasted content",
"errorFormat": "Invalid JSON format",
"save": "Save"
},
"checkParams": "Please check the parameters according to the tutorial.",
"title": "MCP Servers",
"experimental": "Experimental",
"editAllJson": "Edit All Servers JSON",
"findMore": "Find more MCP servers at",
"allowPermissions": "Allow All MCP Tool Permissions",
"allowPermissionsDesc": "When enabled, all MCP tool calls will be automatically approved without showing permission dialogs.",
"noServers": "No MCP servers found",
"args": "Args",
"env": "Env",
"serverStatusActive": "Server {{serverKey}} activated successfully",
"serverStatusInactive": "Server {{serverKey}} deactivated successfully"
}

View File

@ -0,0 +1,5 @@
{
"addProvider": "Add Provider",
"addOpenAIProvider": "Add OpenAI Provider",
"enterNameForProvider": "Enter name for provider"
}

View File

@ -0,0 +1,68 @@
{
"joyride": {
"chooseProviderTitle": "Choose a Provider",
"chooseProviderContent": "Pick the provider you want to use, make sure you have access to an API key for it.",
"getApiKeyTitle": "Get Your API Key",
"getApiKeyContent": "Log into the provider's dashboard to find or generate your API key.",
"insertApiKeyTitle": "Insert Your API Key",
"insertApiKeyContent": "Paste your API key here to connect and activate the provider.",
"back": "Back",
"close": "Close",
"last": "Finish",
"next": "Next",
"skip": "Skip"
},
"refreshModelsError": "Provider must have base URL and API key configured to fetch models.",
"refreshModelsSuccess": "Added {{count}} new model(s) from {{provider}}.",
"noNewModels": "No new models found. All available models are already added.",
"refreshModelsFailed": "Failed to fetch models from {{provider}}. Please check your API key and base URL.",
"models": "Models",
"refreshing": "Refreshing...",
"refresh": "Refresh",
"import": "Import",
"importModelSuccess": "Model {{provider}} has been imported successfully.",
"importModelError": "Failed to import model:",
"stop": "Stop",
"start": "Start",
"noModelFound": "No model found",
"noModelFoundDesc": "Available models will be listed here. If you don't have any models yet, visit the Hub to download.",
"configuration": "Configuration",
"apiEndpoint": "API Endpoint",
"testConnection": "Test Connection",
"addModel": {
"title": "Add New Model",
"description": "Add a new model to the {{provider}} provider.",
"modelId": "Model ID",
"enterModelId": "Enter model ID",
"exploreModels": "See model list from {{provider}}",
"addModel": "Add Model"
},
"deleteModel": {
"title": "Delete Model: {{modelId}}",
"description": "Are you sure you want to delete this model? This action cannot be undone.",
"success": "Model {{modelId}} has been permanently deleted.",
"cancel": "Cancel",
"delete": "Delete"
},
"deleteProvider": {
"title": "Delete Provider",
"description": "Delete this provider and all its models. This action cannot be undone.",
"success": "Provider {{provider}} has been permanently deleted.",
"confirmTitle": "Delete Provider: {{provider}}",
"confirmDescription": "Are you sure you want to delete this provider? This action cannot be undone.",
"cancel": "Cancel",
"delete": "Delete"
},
"editModel": {
"title": "Edit Model: {{modelId}}",
"description": "Configure model capabilities by toggling the options below.",
"capabilities": "Capabilities",
"tools": "Tools",
"vision": "Vision",
"embeddings": "Embeddings",
"notAvailable": "Not available yet"
},
"addProvider": "Add Provider",
"addOpenAIProvider": "Add OpenAI Provider",
"enterNameForProvider": "Enter name for provider"
}

View File

@ -1,19 +1,248 @@
{
"settings": {
"general": {
"autoDownload": "Automatic download new updates"
},
"dataFolder": {
"appData": "App Data",
"appDataDesc": "Default location for messages and other user data.",
"appLogs": "App Logs",
"appLogsDesc": "Default location App Logs."
},
"others": {
"spellCheck": "Spell Check",
"spellCheckDesc": "Enable spell check for your threads.",
"resetFactory": "Reset To Factory Settings",
"resetFactoryDesc": "Restore application to its initial state, erasing all models and chat history. This action is irreversible and recommended only if the application is corrupted."
"autoDownload": "Automatic download new updates",
"checkForUpdates": "Check for Updates",
"checkForUpdatesDesc": "Check if a newer version of Jan is available.",
"checkingForUpdates": "Checking for updates...",
"noUpdateAvailable": "You're running the latest version",
"devVersion": "Development version detected",
"updateError": "Failed to check for updates",
"changeLocation": "Change Location",
"copied": "Copied",
"copyPath": "Copy Path",
"openLogs": "Open Logs",
"revealLogs": "Reveal Logs",
"showInFinder": "Show in Finder",
"showInFileExplorer": "Show in File Explorer",
"openContainingFolder": "Open Containing Folder",
"failedToRelocateDataFolder": "Failed to relocate data folder",
"failedToRelocateDataFolderDesc": "Failed to relocate data folder. Please try again.",
"factoryResetTitle": "Reset to Factory Settings",
"factoryResetDesc": "This will reset all app settings to their defaults. This can't be undone. We only recommend this if the app is corrupted.",
"cancel": "Cancel",
"reset": "Reset",
"resources": "Resources",
"documentation": "Documentation",
"documentationDesc": "Learn how to use Jan and explore its features.",
"viewDocs": "View Docs",
"releaseNotes": "Release Notes",
"releaseNotesDesc": "See what's new in the latest version of Jan.",
"viewReleases": "View Releases",
"community": "Community",
"github": "GitHub",
"githubDesc": "Contribute to Jan's development.",
"discord": "Discord",
"discordDesc": "Join our community for support and discussions.",
"support": "Support",
"reportAnIssue": "Report an Issue",
"reportAnIssueDesc": "Found a bug? Help us out by filing an issue on GitHub.",
"reportIssue": "Report Issue",
"credits": "Credits",
"creditsDesc1": "Jan is built with ❤️ by the Menlo Team.",
"creditsDesc2": "Special thanks to our open-source dependencies—especially llama.cpp and Tauri—and to our amazing AI community.",
"appVersion": "App Version",
"dataFolder": {
"appData": "App Data",
"appDataDesc": "Default location for messages and other user data.",
"appLogs": "App Logs",
"appLogsDesc": "View detailed logs of the App."
},
"others": {
"spellCheck": "Spell Check",
"spellCheckDesc": "Enable spell check for your threads.",
"resetFactory": "Reset To Factory Settings",
"resetFactoryDesc": "Restore application to its initial state, erasing all models and chat history. This action is irreversible and recommended only if the application is corrupted."
},
"shortcuts": {
"application": "Application",
"newChat": "New Chat",
"newChatDesc": "Create a new chat.",
"toggleSidebar": "Toggle Sidebar",
"toggleSidebarDesc": "Show or hide the sidebar.",
"zoomIn": "Zoom In",
"zoomInDesc": "Increase the zoom level.",
"zoomOut": "Zoom Out",
"zoomOutDesc": "Decrease the zoom level.",
"chat": "Chat",
"sendMessage": "Send Message",
"sendMessageDesc": "Send the current message.",
"enter": "Enter",
"newLine": "New Line",
"newLineDesc": "Insert a new line.",
"shiftEnter": "Shift + Enter",
"navigation": "Navigation",
"goToSettings": "Go to Settings",
"goToSettingsDesc": "Open settings."
},
"appearance": {
"title": "Appearance",
"theme": "Theme",
"themeDesc": "Match the OS theme.",
"fontSize": "Font Size",
"fontSizeDesc": "Adjust the app's font size.",
"windowBackground": "Window Background",
"windowBackgroundDesc": "Set the app window's background color.",
"appMainView": "App Main View",
"appMainViewDesc": "Set the main content area's background color.",
"primary": "Primary",
"primaryDesc": "Set the primary color for UI components.",
"accent": "Accent",
"accentDesc": "Set the accent color for UI highlights.",
"destructive": "Destructive",
"destructiveDesc": "Set the color for destructive actions.",
"resetToDefault": "Reset to Default",
"resetToDefaultDesc": "Reset all appearance settings to default.",
"resetAppearanceSuccess": "Appearance reset successfully",
"resetAppearanceSuccessDesc": "All appearance settings have been restored to default.",
"chatWidth": "Chat Width",
"chatWidthDesc": "Customize the width of the chat view.",
"codeBlockTitle": "Code Block",
"codeBlockDesc": "Choose a syntax highlighting style.",
"showLineNumbers": "Show Line Numbers",
"showLineNumbersDesc": "Display line numbers in code blocks.",
"resetCodeBlockStyle": "Reset Code Block Style",
"resetCodeBlockStyleDesc": "Reset code block style to default.",
"resetCodeBlockSuccess": "Code block style reset successfully",
"resetCodeBlockSuccessDesc": "Code block style has been restored to default."
},
"hardware": {
"os": "Operating System",
"name": "Name",
"version": "Version",
"cpu": "CPU",
"model": "Model",
"architecture": "Architecture",
"cores": "Cores",
"instructions": "Instructions",
"usage": "Usage",
"memory": "Memory",
"totalRam": "Total RAM",
"availableRam": "Available RAM",
"vulkan": "Vulkan",
"enableVulkan": "Enable Vulkan",
"enableVulkanDesc": "Use Vulkan API for GPU acceleration. Do not enable Vulkan if you have an NVIDIA GPU as it may cause compatibility issues.",
"gpus": "GPUs",
"noGpus": "No GPUs detected",
"vram": "VRAM",
"freeOf": "free of",
"driverVersion": "Driver Version",
"computeCapability": "Compute Capability",
"systemMonitor": "System Monitor"
},
"httpsProxy": {
"proxy": "Proxy",
"proxyUrl": "Proxy URL",
"proxyUrlDesc": "The URL and port of your proxy server.",
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
"authentication": "Authentication",
"authenticationDesc": "Credentials for the proxy server, if required.",
"username": "Username",
"password": "Password",
"noProxy": "No Proxy",
"noProxyDesc": "A comma-separated list of hosts to bypass the proxy.",
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
"sslVerification": "SSL Verification",
"ignoreSsl": "Ignore SSL Certificates",
"ignoreSslDesc": "Allow self-signed or unverified certificates. This may be required for some proxies but reduces security. Only enable if you trust your proxy.",
"proxySsl": "Proxy SSL",
"proxySslDesc": "Validate the SSL certificate when connecting to the proxy.",
"proxyHostSsl": "Proxy Host SSL",
"proxyHostSslDesc": "Validate the SSL certificate of the proxy's host.",
"peerSsl": "Peer SSL",
"peerSslDesc": "Validate the SSL certificates of peer connections.",
"hostSsl": "Host SSL",
"hostSslDesc": "Validate the SSL certificates of destination hosts."
},
"localApiServer": {
"title": "Local API Server",
"description": "Run an OpenAI-compatible server locally.",
"startServer": "Start Server",
"stopServer": "Stop Server",
"serverLogs": "Server Logs",
"serverLogsDesc": "View detailed logs of the local API server.",
"openLogs": "Open Logs",
"serverConfiguration": "Server Configuration",
"serverHost": "Server Host",
"serverHostDesc": "Network address for the server.",
"serverPort": "Server Port",
"serverPortDesc": "Port number for the API server.",
"apiPrefix": "API Prefix",
"apiPrefixDesc": "Path prefix for API endpoints.",
"apiKey": "API Key",
"apiKeyDesc": "Authenticate requests with an API key.",
"trustedHosts": "Trusted Hosts",
"trustedHostsDesc": "Hosts allowed to access the server, separated by commas.",
"advancedSettings": "Advanced Settings",
"cors": "Cross-Origin Resource Sharing (CORS)",
"corsDesc": "Allow cross-origin requests to the API server.",
"verboseLogs": "Verbose Server Logs",
"verboseLogsDesc": "Enable detailed server logs for debugging."
},
"privacy": {
"analytics": "Analytics",
"helpUsImprove": "Help us improve",
"helpUsImproveDesc": "To help us improve Jan, you can share anonymous data like feature usage and user counts. We never collect your chats or personal information.",
"privacyPolicy": "You have full control over your data. Learn more in our Privacy Policy.",
"analyticsDesc": "To improve Jan, we need to understand how it's used—but only with your help. You can change this setting anytime.",
"privacyPromises": "Your choice here won't change our core privacy promises:",
"promise1": "Your conversations stay private and on your device",
"promise2": "We never collect your personal information or chat content",
"promise3": "All data sharing is anonymous and aggregated",
"promise4": "You can opt out anytime without losing functionality",
"promise5": "We're transparent about what we collect and why"
},
"general": {
"showInFinder": "Show in Finder",
"showInFileExplorer": "Show in File Explorer",
"openContainingFolder": "Open Containing Folder",
"failedToRelocateDataFolder": "Failed to relocate data folder",
"failedToRelocateDataFolderDesc": "Failed to relocate data folder. Please try again.",
"devVersion": "Development version detected",
"noUpdateAvailable": "You're running the latest version",
"updateError": "Failed to check for updates",
"appVersion": "App Version",
"checkForUpdates": "Check for Updates",
"checkForUpdatesDesc": "Check if a newer version of Jan is available.",
"checkingForUpdates": "Checking for updates...",
"copied": "Copied",
"copyPath": "Copy Path",
"changeLocation": "Change Location",
"openLogs": "Open Logs",
"revealLogs": "Reveal Logs",
"factoryResetTitle": "Reset to Factory Settings",
"factoryResetDesc": "This will reset all app settings to their defaults. This can't be undone. We only recommend this if the app is corrupted.",
"cancel": "Cancel",
"reset": "Reset",
"resources": "Resources",
"documentation": "Documentation",
"documentationDesc": "Learn how to use Jan and explore its features.",
"viewDocs": "View Docs",
"releaseNotes": "Release Notes",
"releaseNotesDesc": "See what's new in the latest version of Jan.",
"viewReleases": "View Releases",
"community": "Community",
"github": "GitHub",
"githubDesc": "Contribute to Jan's development.",
"discord": "Discord",
"discordDesc": "Join our community for support and discussions.",
"support": "Support",
"reportAnIssue": "Report an Issue",
"reportAnIssueDesc": "Found a bug? Help us out by filing an issue on GitHub.",
"reportIssue": "Report Issue",
"credits": "Credits",
"creditsDesc1": "Jan is built with ❤️ by the Menlo Team.",
"creditsDesc2": "Special thanks to our open-source dependencies—especially llama.cpp and Tauri—and to our amazing AI community."
},
"extensions": {
"title": "Extensions"
},
"dialogs": {
"changeDataFolder": {
"title": "Change Data Folder Location",
"description": "Are you sure you want to change the data folder location? This will move all your data to the new location and restart the application.",
"currentLocation": "Current Location:",
"newLocation": "New Location:",
"cancel": "Cancel",
"changeLocation": "Change Location"
}
}
}

View File

@ -0,0 +1,6 @@
{
"welcome": "Welcome to Jan",
"description": "To get started, you'll need to either download a local AI model or connect to a cloud model using an API key",
"localModel": "Set up local model",
"remoteProvider": "Set up remote provider"
}

View File

@ -0,0 +1,28 @@
{
"title": "System Monitor",
"cpuUsage": "CPU Usage",
"model": "Model",
"cores": "Cores",
"architecture": "Architecture",
"currentUsage": "Current Usage",
"memoryUsage": "Memory Usage",
"totalRam": "Total RAM",
"availableRam": "Available RAM",
"usedRam": "Used RAM",
"runningModels": "Running Models",
"noRunningModels": "No models are currently running",
"provider": "Provider",
"uptime": "Uptime",
"actions": "Actions",
"stop": "Stop",
"activeGpus": "Active GPUs",
"noGpus": "No GPUs detected",
"noActiveGpus": "No active GPUs. All GPUs are currently disabled.",
"vramUsage": "VRAM Usage",
"driverVersion": "Driver Version:",
"computeCapability": "Compute Capability:",
"active": "Active",
"performance": "Performance",
"resources": "Resources",
"refresh": "Refresh"
}

View File

@ -0,0 +1,11 @@
{
"title": "Tool Call Request",
"description": "The assistant wants to use the tool: <strong>{{toolName}}</strong>",
"securityNotice": "<strong>Security Notice:</strong> Malicious tools or conversation content could potentially trick the assistant into attempting harmful actions. Review each tool call carefully before approving.",
"deny": "Deny",
"allowOnce": "Allow Once",
"alwaysAllow": "Always Allow",
"permissions": "Permissions",
"approve": "Approve",
"reject": "Reject"
}

View File

@ -0,0 +1,10 @@
{
"toolApproval": {
"title": "Tool Approval Required",
"description": "The assistant wants to use <strong>{{toolName}}</strong>",
"securityNotice": "This tool wants to perform an action. Please review and approve.",
"deny": "Deny",
"allowOnce": "Allow Once",
"alwaysAllow": "Always Allow"
}
}

View File

@ -0,0 +1,10 @@
{
"newVersion": "New Version {{version}}",
"updateAvailable": "Update Available",
"nightlyBuild": "Nightly Build",
"showReleaseNotes": "Show Release Notes",
"hideReleaseNotes": "Hide Release Notes",
"remindMeLater": "Remind Me Later",
"downloading": "Downloading...",
"updateNow": "Update Now"
}

View File

@ -0,0 +1,32 @@
{
"title": "Asisten",
"editAssistant": "Edit Asisten",
"deleteAssistant": "Hapus Asisten",
"deleteConfirmation": "Hapus Asisten",
"deleteConfirmationDesc": "Apakah Anda yakin ingin menghapus asisten ini? Tindakan ini tidak dapat dibatalkan.",
"cancel": "Batal",
"delete": "Hapus",
"addAssistant": "Tambah Asisten",
"emoji": "Emoji",
"name": "Nama",
"enterName": "Masukkan nama",
"description": "Deskripsi (opsional)",
"enterDescription": "Masukkan deskripsi",
"instructions": "Instruksi",
"enterInstructions": "Masukkan instruksi",
"predefinedParameters": "Parameter yang telah ditentukan",
"parameters": "Parameter",
"key": "Kunci",
"value": "Nilai",
"stringValue": "String",
"numberValue": "Angka",
"booleanValue": "Boolean",
"jsonValue": "JSON",
"trueValue": "Benar",
"falseValue": "Salah",
"jsonValuePlaceholder": "Nilai JSON",
"save": "Simpan",
"createNew": "Buat Asisten Baru",
"personality": "Kepribadian",
"capabilities": "Kemampuan"
}

View File

@ -1,9 +1,10 @@
{
"chat": {
"welcome": "Hai, apa kabar?",
"description": "Apa yang bisa saya bantu hari ini?",
"status": {
"empty": "Tidak ada percakapan ditemukan"
}
}
}
"welcome": "Hai, apa kabar?",
"description": "Ada yang bisa saya bantu hari ini?",
"status": {
"empty": "Tidak Ada Obrolan Ditemukan"
},
"sendMessage": "Kirim Pesan",
"newConversation": "Percakapan Baru",
"clearHistory": "Hapus Riwayat"
}

View File

@ -1,31 +1,267 @@
{
"common": {
"general": "Umum",
"settings": "Pengaturan",
"modelProviders": "Penyedia Model",
"appearance": "Tampilan",
"privacy": "Privasi",
"keyboardShortcuts": "Pintasan",
"newChat": "Obrolan Baru",
"favorites": "Favorit",
"recents": "Terbaru",
"hub": "Hub",
"helpSupport": "Bantuan & Dukungan",
"unstarAll": "Hapus Bintang",
"unstar": "Hapus Bintang",
"deleteAll": "Hapus Semua",
"star": "Bintang",
"rename": "Ganti Nama",
"delete": "Hapus",
"dataFolder": "Folder Data",
"others": "Lainnya",
"language": "Bahasa",
"reset": "Atur Ulang",
"search": "Cari",
"name": "Nama",
"placeholder": {
"chatInput": "Tanya apa saja..."
"assistants": "Asisten",
"hardware": "Perangkat Keras",
"mcp-servers": "Server MCP",
"local_api_server": "Server API Lokal",
"https_proxy": "Proksi HTTPS",
"extensions": "Ekstensi",
"general": "Umum",
"settings": "Pengaturan",
"modelProviders": "Penyedia Model",
"appearance": "Tampilan",
"privacy": "Privasi",
"keyboardShortcuts": "Pintasan",
"newChat": "Obrolan Baru",
"favorites": "Favorit",
"recents": "Terbaru",
"hub": "Hub",
"helpSupport": "Bantuan & Dukungan",
"helpUsImproveJan": "Bantu Kami Meningkatkan Jan",
"unstarAll": "Batal Bintangi Semua",
"unstar": "Batal Bintangi",
"deleteAll": "Hapus Semua",
"star": "Bintangi",
"rename": "Ubah Nama",
"delete": "Hapus",
"copied": "Tersalin!",
"dataFolder": "Folder Data",
"others": "Lainnya",
"language": "Bahasa",
"reset": "Setel Ulang",
"search": "Cari",
"name": "Nama",
"cancel": "Batal",
"create": "Buat",
"save": "Simpan",
"edit": "Edit",
"copy": "Salin",
"back": "Kembali",
"close": "Tutup",
"next": "Berikutnya",
"finish": "Selesai",
"skip": "Lewati",
"allow": "Izinkan",
"deny": "Tolak",
"start": "Mulai",
"stop": "Hentikan",
"preview": "Pratinjau",
"compactWidth": "Lebar Ringkas",
"fullWidth": "Lebar Penuh",
"dark": "Gelap",
"light": "Terang",
"system": "Sistem",
"auto": "Otomatis",
"english": "Inggris",
"medium": "Sedang",
"newThread": "Utas Baru",
"noResultsFound": "Tidak ada hasil yang ditemukan",
"noThreadsYet": "Belum ada utas",
"noThreadsYetDesc": "Mulai percakapan baru untuk melihat riwayat utas Anda di sini.",
"downloads": "Unduhan",
"downloading": "Mengunduh",
"cancelDownload": "Batalkan unduhan",
"downloadCancelled": "Unduhan Dibatalkan",
"downloadComplete": "Unduhan Selesai",
"thinking": "Berpikir...",
"thought": "Pikiran",
"callingTool": "Memanggil alat",
"completed": "Selesai",
"image": "Gambar",
"vision": "Visi",
"embeddings": "Embedding",
"tools": "Alat",
"webSearch": "Pencarian Web",
"reasoning": "Penalaran",
"selectAModel": "Pilih model",
"noToolsAvailable": "Tidak ada alat yang tersedia",
"noModelsFoundFor": "Tidak ada model yang ditemukan untuk \"{{searchValue}}\"",
"customAvatar": "Avatar kustom",
"editAssistant": "Edit Asisten",
"jan": "Jan",
"metadata": "Metadata",
"regenerate": "Hasilkan Ulang",
"threadImage": "Gambar utas",
"editMessage": "Edit Pesan",
"deleteMessage": "Hapus Pesan",
"deleteThread": "Hapus Utas",
"renameThread": "Ubah Nama Utas",
"threadTitle": "Judul Utas",
"deleteAllThreads": "Hapus Semua Utas",
"allThreadsUnfavorited": "Semua Utas Batal Difavoritkan",
"deleteAllThreadsConfirm": "Apakah Anda yakin ingin menghapus semua utas? Tindakan ini tidak dapat dibatalkan.",
"addProvider": "Tambah Penyedia",
"addOpenAIProvider": "Tambah Penyedia OpenAI",
"enterNameForProvider": "Masukkan nama untuk penyedia Anda",
"providerAlreadyExists": "Penyedia dengan nama \"{{name}}\" sudah ada. Silakan pilih nama yang berbeda.",
"adjustFontSize": "Sesuaikan Ukuran Font",
"changeLanguage": "Ubah Bahasa",
"editTheme": "Edit Tema",
"editCodeBlockStyle": "Edit Gaya Blok Kode",
"editServerHost": "Edit Host Server",
"pickColorWindowBackground": "Pilih Warna Latar Belakang Jendela",
"pickColorAppMainView": "Pilih Warna Tampilan Utama Aplikasi",
"pickColorAppPrimary": "Pilih Warna Utama Aplikasi",
"pickColorAppAccent": "Pilih Warna Aksen Aplikasi",
"pickColorAppDestructive": "Pilih Warna Merusak Aplikasi",
"apiKeyRequired": "Kunci API diperlukan",
"enterTrustedHosts": "Masukkan host tepercaya",
"placeholder": {
"chatInput": "Tanyakan apa saja padaku..."
},
"confirm": "Konfirmasi",
"loading": "Memuat...",
"error": "Kesalahan",
"success": "Sukses",
"warning": "Peringatan",
"noResultsFoundDesc": "Kami tidak dapat menemukan obrolan yang cocok dengan pencarian Anda. Coba kata kunci yang berbeda.",
"searchModels": "Cari model...",
"searchStyles": "Cari gaya...",
"createAssistant": "Buat Asisten",
"enterApiKey": "Masukkan Kunci API",
"scrollToBottom": "Gulir ke bawah",
"addModel": {
"title": "Tambah Model",
"modelId": "ID Model",
"enterModelId": "Masukkan ID Model",
"addModel": "Tambah Model",
"description": "Tambahkan model baru ke penyedia",
"exploreModels": "Lihat daftar model dari penyedia"
},
"mcpServers": {
"editServer": "Edit Server",
"addServer": "Tambah Server",
"serverName": "Nama Server",
"enterServerName": "Masukkan nama server",
"command": "Perintah",
"enterCommand": "Masukkan perintah",
"arguments": "Argumen",
"argument": "Argumen {{index}}",
"envVars": "Variabel Lingkungan",
"key": "Kunci",
"value": "Nilai",
"save": "Simpan"
},
"deleteServer": {
"title": "Hapus Server",
"delete": "Hapus"
},
"editJson": {
"errorParse": "Gagal mengurai JSON",
"errorPaste": "Gagal menempelkan JSON",
"errorFormat": "Format JSON tidak valid",
"titleAll": "Edit Semua Konfigurasi Server",
"placeholder": "Masukkan konfigurasi JSON...",
"save": "Simpan"
},
"editModel": {
"title": "Edit Model: {{modelId}}",
"description": "Konfigurasikan kemampuan model dengan mengalihkan opsi di bawah ini.",
"capabilities": "Kemampuan",
"tools": "Alat",
"vision": "Visi",
"embeddings": "Embedding",
"notAvailable": "Belum tersedia"
},
"toolApproval": {
"title": "Permintaan Izin Alat",
"description": "Asisten ingin menggunakan <strong>{{toolName}}</strong>",
"securityNotice": "Hanya izinkan alat yang Anda percayai. Alat dapat mengakses sistem dan data Anda.",
"deny": "Tolak",
"allowOnce": "Izinkan Sekali",
"alwaysAllow": "Selalu Izinkan"
},
"deleteModel": {
"title": "Hapus Model: {{modelId}}",
"description": "Apakah Anda yakin ingin menghapus model ini? Tindakan ini tidak dapat dibatalkan.",
"success": "Model {{modelId}} telah dihapus secara permanen.",
"cancel": "Batal",
"delete": "Hapus"
},
"deleteProvider": {
"title": "Hapus Penyedia",
"description": "Hapus penyedia ini dan semua modelnya. Tindakan ini tidak dapat dibatalkan.",
"success": "Penyedia {{provider}} telah dihapus secara permanen.",
"confirmTitle": "Hapus Penyedia: {{provider}}",
"confirmDescription": "Apakah Anda yakin ingin menghapus penyedia ini? Tindakan ini tidak dapat dibatalkan.",
"cancel": "Batal",
"delete": "Hapus"
},
"modelSettings": {
"title": "Pengaturan Model - {{modelId}}",
"description": "Konfigurasikan pengaturan model untuk mengoptimalkan kinerja dan perilaku."
},
"dialogs": {
"changeDataFolder": {
"title": "Ubah Lokasi Folder Data",
"description": "Apakah Anda yakin ingin mengubah lokasi folder data? Ini akan memindahkan semua data Anda ke lokasi baru dan memulai ulang aplikasi.",
"currentLocation": "Lokasi Saat Ini:",
"newLocation": "Lokasi Baru:",
"cancel": "Batal",
"changeLocation": "Ubah Lokasi"
},
"deleteAllThreads": {
"title": "Hapus Semua Utas",
"description": "Semua utas akan dihapus. Tindakan ini tidak dapat dibatalkan."
},
"deleteThread": {
"description": "Apakah Anda yakin ingin menghapus thread ini? Tindakan ini tidak dapat dibatalkan."
},
"editMessage": {
"title": "Edit Pesan"
},
"messageMetadata": {
"title": "Metadata Pesan"
}
},
"toast": {
"allThreadsUnfavorited": {
"title": "Semua Utas Batal Difavoritkan",
"description": "Semua utas telah dihapus dari favorit Anda."
},
"deleteAllThreads": {
"title": "Hapus Semua Utas",
"description": "Semua utas telah dihapus secara permanen."
},
"renameThread": {
"title": "Ubah Nama Utas",
"description": "Judul utas telah diubah menjadi '{{title}}'"
},
"deleteThread": {
"title": "Hapus Utas",
"description": "Utas ini telah dihapus secara permanen."
},
"editMessage": {
"title": "Edit Pesan",
"description": "Pesan berhasil diedit. Silakan tunggu model merespons."
},
"appUpdateDownloaded": {
"title": "Pembaruan Aplikasi Diunduh",
"description": "Pembaruan aplikasi telah berhasil diunduh."
},
"appUpdateDownloadFailed": {
"title": "Gagal Mengunduh Pembaruan Aplikasi",
"description": "Gagal mengunduh pembaruan aplikasi. Silakan coba lagi."
},
"downloadComplete": {
"title": "Unduhan Selesai",
"description": "Model {{modelId}} telah diunduh"
},
"downloadCancelled": {
"title": "Unduhan Dibatalkan",
"description": "Proses unduhan telah dibatalkan"
}
},
"cortexFailureDialog": {
"title": "Cortex gagal dimulai",
"description": "Cortex gagal dimulai. Silakan periksa log untuk detail lebih lanjut.",
"contactSupport": "Hubungi Dukungan",
"restartJan": "Restart Jan"
},
"outOfContextError": {
"title": "Kesalahan di luar konteks",
"description": "Obrolan ini mencapai batas memori AI, seperti papan tulis yang penuh. Kami dapat memperluas jendela memori (disebut ukuran konteks) sehingga mengingat lebih banyak, tetapi mungkin menggunakan lebih banyak memori komputer Anda. Kami juga dapat memotong input, yang berarti akan melupakan sebagian riwayat obrolan untuk memberi ruang bagi pesan baru.",
"increaseContextSizeDescription": "Apakah Anda ingin meningkatkan ukuran konteks?",
"truncateInput": "Potong Input",
"increaseContextSize": "Tingkatkan Ukuran Konteks"
}
}

View File

@ -0,0 +1,31 @@
{
"sortNewest": "Terbaru",
"sortMostDownloaded": "Paling Banyak Diunduh",
"use": "Gunakan",
"download": "Unduh",
"downloaded": "Telah Diunduh",
"loadingModels": "Memuat model...",
"noModels": "Tidak ada model yang ditemukan",
"by": "oleh",
"downloads": "Unduhan",
"variants": "Varian",
"showVariants": "Tampilkan Varian",
"useModel": "Gunakan model ini",
"downloadModel": "Unduh model",
"searchPlaceholder": "Cari model di Hugging Face...",
"editTheme": "Edit Tema",
"joyride": {
"recommendedModelTitle": "Model yang Direkomendasikan",
"recommendedModelContent": "Jelajahi dan unduh model AI yang kuat dari berbagai penyedia, semuanya di satu tempat. Kami sarankan memulai dengan Jan-Nano - model yang dioptimalkan untuk pemanggilan fungsi, integrasi alat, dan kemampuan penelitian. Ini ideal untuk membangun agen AI interaktif.",
"downloadInProgressTitle": "Unduhan Sedang Berlangsung",
"downloadInProgressContent": "Model Anda sedang diunduh. Lacak kemajuan di sini - setelah selesai, model akan siap digunakan.",
"downloadModelTitle": "Unduh Model",
"downloadModelContent": "Klik tombol Unduh untuk mulai mengunduh model.",
"back": "Kembali",
"close": "Tutup",
"lastWithDownload": "Unduh",
"last": "Selesai",
"next": "Berikutnya",
"skip": "Lewati"
}
}

View File

@ -0,0 +1,3 @@
{
"noLogs": "Tidak ada log yang tersedia"
}

View File

@ -0,0 +1,43 @@
{
"editServer": "Edit Server MCP",
"addServer": "Tambah Server MCP",
"serverName": "Nama Server",
"enterServerName": "Masukkan nama server",
"command": "Perintah",
"enterCommand": "Masukkan perintah (uvx atau npx)",
"arguments": "Argumen",
"argument": "Argumen {{index}}",
"envVars": "Variabel Lingkungan",
"key": "Kunci",
"value": "Nilai",
"save": "Simpan",
"status": "Status",
"connected": "Terhubung",
"disconnected": "Terputus",
"deleteServer": {
"title": "Hapus Server MCP",
"description": "Apakah Anda yakin ingin menghapus server MCP {{serverName}}? Tindakan ini tidak dapat dibatalkan.",
"delete": "Hapus"
},
"editJson": {
"title": "Edit JSON untuk Server MCP: {{serverName}}",
"titleAll": "Edit Semua JSON Server MCP",
"placeholder": "Masukkan konfigurasi JSON",
"errorParse": "Gagal mengurai data awal",
"errorPaste": "Format JSON tidak valid pada konten yang ditempel",
"errorFormat": "Format JSON tidak valid",
"save": "Simpan"
},
"checkParams": "Silakan periksa parameter sesuai dengan tutorial.",
"title": "Server MCP",
"experimental": "Eksperimental",
"editAllJson": "Edit Semua JSON Server",
"findMore": "Temukan lebih banyak server MCP di",
"allowPermissions": "Izinkan Semua Izin Alat MCP",
"allowPermissionsDesc": "Jika diaktifkan, semua panggilan alat MCP akan disetujui secara otomatis tanpa menampilkan dialog izin.",
"noServers": "Tidak ada server MCP yang ditemukan",
"args": "Argumen",
"env": "Lingkungan",
"serverStatusActive": "Server {{serverKey}} berhasil diaktifkan",
"serverStatusInactive": "Server {{serverKey}} berhasil dinonaktifkan"
}

View File

@ -0,0 +1,5 @@
{
"addProvider": "Tambah Penyedia",
"addOpenAIProvider": "Tambah Penyedia OpenAI",
"enterNameForProvider": "Masukkan nama untuk penyedia"
}

View File

@ -0,0 +1,68 @@
{
"joyride": {
"chooseProviderTitle": "Pilih Penyedia",
"chooseProviderContent": "Pilih penyedia yang ingin Anda gunakan, pastikan Anda memiliki akses ke kunci API untuk itu.",
"getApiKeyTitle": "Dapatkan Kunci API Anda",
"getApiKeyContent": "Masuk ke dasbor penyedia untuk menemukan atau membuat kunci API Anda.",
"insertApiKeyTitle": "Masukkan Kunci API Anda",
"insertApiKeyContent": "Tempel kunci API Anda di sini untuk menghubungkan dan mengaktifkan penyedia.",
"back": "Kembali",
"close": "Tutup",
"last": "Selesai",
"next": "Berikutnya",
"skip": "Lewati"
},
"refreshModelsError": "Penyedia harus memiliki URL dasar dan kunci API yang dikonfigurasi untuk mengambil model.",
"refreshModelsSuccess": "Menambahkan {{count}} model baru dari {{provider}}.",
"noNewModels": "Tidak ada model baru yang ditemukan. Semua model yang tersedia sudah ditambahkan.",
"refreshModelsFailed": "Gagal mengambil model dari {{provider}}. Silakan periksa kunci API dan URL dasar Anda.",
"models": "Model",
"refreshing": "Menyegarkan...",
"refresh": "Segarkan",
"import": "Impor",
"importModelSuccess": "Model {{provider}} telah berhasil diimpor.",
"importModelError": "Gagal mengimpor model:",
"stop": "Hentikan",
"start": "Mulai",
"noModelFound": "Tidak ada model yang ditemukan",
"noModelFoundDesc": "Model yang tersedia akan dicantumkan di sini. Jika Anda belum memiliki model, kunjungi Hub untuk mengunduh.",
"configuration": "Konfigurasi",
"apiEndpoint": "Titik Akhir API",
"testConnection": "Uji Koneksi",
"addModel": {
"title": "Tambah Model Baru",
"description": "Tambahkan model baru ke penyedia {{provider}}.",
"modelId": "ID Model",
"enterModelId": "Masukkan ID model",
"exploreModels": "Lihat daftar model dari {{provider}}",
"addModel": "Tambah Model"
},
"deleteModel": {
"title": "Hapus Model: {{modelId}}",
"description": "Apakah Anda yakin ingin menghapus model ini? Tindakan ini tidak dapat dibatalkan.",
"success": "Model {{modelId}} telah dihapus secara permanen.",
"cancel": "Batal",
"delete": "Hapus"
},
"deleteProvider": {
"title": "Hapus Penyedia",
"description": "Hapus penyedia ini dan semua modelnya. Tindakan ini tidak dapat dibatalkan.",
"success": "Penyedia {{provider}} telah dihapus secara permanen.",
"confirmTitle": "Hapus Penyedia: {{provider}}",
"confirmDescription": "Apakah Anda yakin ingin menghapus penyedia ini? Tindakan ini tidak dapat dibatalkan.",
"cancel": "Batal",
"delete": "Hapus"
},
"editModel": {
"title": "Edit Model: {{modelId}}",
"description": "Konfigurasikan kemampuan model dengan mengalihkan opsi di bawah ini.",
"capabilities": "Kemampuan",
"tools": "Alat",
"vision": "Visi",
"embeddings": "Embedding",
"notAvailable": "Belum tersedia"
},
"addProvider": "Tambah Penyedia",
"addOpenAIProvider": "Tambah Penyedia OpenAI",
"enterNameForProvider": "Masukkan nama untuk penyedia"
}

View File

@ -1,19 +1,248 @@
{
"settings": {
"general": {
"autoDownload": "Unduh pembaruan baru secara otomatis"
},
"dataFolder": {
"appData": "Data Aplikasi",
"appDataDesc": "Lokasi default untuk pesan dan data pengguna lainnya",
"appLogs": "Log Aplikasi",
"appLogsDesc": "Lokasi default Log Aplikasi"
},
"others": {
"spellCheck": "Pemeriksaan Ejaan",
"spellCheckDesc": "Aktifkan untuk mengaktifkan pemeriksaan ejaan input chat.",
"resetFactory": "Atur Ulang ke Pengaturan Pabrik",
"resetFactoryDesc": "Kembalikan aplikasi ke keadaan awal, menghapus semua model dan riwayat chat. Tindakan ini tidak dapat diubah dan hanya disarankan jika aplikasi rusak"
"autoDownload": "Unduh pembaruan baru secara otomatis",
"checkForUpdates": "Periksa Pembaruan",
"checkForUpdatesDesc": "Periksa apakah versi Jan yang lebih baru tersedia.",
"checkingForUpdates": "Memeriksa pembaruan...",
"noUpdateAvailable": "Anda menjalankan versi terbaru",
"devVersion": "Versi pengembangan terdeteksi",
"updateError": "Gagal memeriksa pembaruan",
"changeLocation": "Ubah Lokasi",
"copied": "Tersalin",
"copyPath": "Salin Jalur",
"openLogs": "Buka Log",
"revealLogs": "Tampilkan Log",
"showInFinder": "Tampilkan di Finder",
"showInFileExplorer": "Tampilkan di File Explorer",
"openContainingFolder": "Buka Folder Induk",
"failedToRelocateDataFolder": "Gagal memindahkan folder data",
"failedToRelocateDataFolderDesc": "Gagal memindahkan folder data. Silakan coba lagi.",
"factoryResetTitle": "Setel Ulang ke Pengaturan Pabrik",
"factoryResetDesc": "Ini akan mengatur ulang semua pengaturan aplikasi ke default. Tindakan ini tidak dapat dibatalkan. Kami hanya merekomendasikan ini jika aplikasi rusak.",
"cancel": "Batal",
"reset": "Setel Ulang",
"resources": "Sumber Daya",
"documentation": "Dokumentasi",
"documentationDesc": "Pelajari cara menggunakan Jan dan jelajahi fitur-fiturnya.",
"viewDocs": "Lihat Dokumentasi",
"releaseNotes": "Catatan Rilis",
"releaseNotesDesc": "Lihat apa yang baru di versi terbaru Jan.",
"viewReleases": "Lihat Rilis",
"community": "Komunitas",
"github": "GitHub",
"githubDesc": "Berkontribusi pada pengembangan Jan.",
"discord": "Discord",
"discordDesc": "Bergabunglah dengan komunitas kami untuk dukungan dan diskusi.",
"support": "Dukungan",
"reportAnIssue": "Laporkan Masalah",
"reportAnIssueDesc": "Menemukan bug? Bantu kami dengan mengajukan masalah di GitHub.",
"reportIssue": "Laporkan Masalah",
"credits": "Kredit",
"creditsDesc1": "Jan dibuat dengan ❤️ oleh Tim Menlo.",
"creditsDesc2": "Terima kasih khusus kepada dependensi sumber terbuka kami—terutama llama.cpp dan Tauri—dan kepada komunitas AI kami yang luar biasa.",
"appVersion": "Versi Aplikasi",
"dataFolder": {
"appData": "Data Aplikasi",
"appDataDesc": "Lokasi default untuk pesan dan data pengguna lainnya.",
"appLogs": "Log Aplikasi",
"appLogsDesc": "Lihat log terperinci dari Aplikasi."
},
"others": {
"spellCheck": "Pemeriksaan Ejaan",
"spellCheckDesc": "Aktifkan pemeriksaan ejaan untuk utas Anda.",
"resetFactory": "Setel Ulang ke Pengaturan Pabrik",
"resetFactoryDesc": "Kembalikan aplikasi ke keadaan semula, menghapus semua model dan riwayat obrolan. Tindakan ini tidak dapat diurungkan dan hanya disarankan jika aplikasi rusak."
},
"shortcuts": {
"application": "Aplikasi",
"newChat": "Obrolan Baru",
"newChatDesc": "Buat obrolan baru.",
"toggleSidebar": "Beralih Bilah Sisi",
"toggleSidebarDesc": "Tampilkan atau sembunyikan bilah sisi.",
"zoomIn": "Perbesar",
"zoomInDesc": "Tingkatkan tingkat zoom.",
"zoomOut": "Perkecil",
"zoomOutDesc": "Kurangi tingkat zoom.",
"chat": "Obrolan",
"sendMessage": "Kirim Pesan",
"sendMessageDesc": "Kirim pesan saat ini.",
"enter": "Enter",
"newLine": "Baris Baru",
"newLineDesc": "Sisipkan baris baru.",
"shiftEnter": "Shift + Enter",
"navigation": "Navigasi",
"goToSettings": "Buka Pengaturan",
"goToSettingsDesc": "Buka pengaturan."
},
"appearance": {
"title": "Tampilan",
"theme": "Tema",
"themeDesc": "Sesuaikan dengan tema OS.",
"fontSize": "Ukuran Font",
"fontSizeDesc": "Sesuaikan ukuran font aplikasi.",
"windowBackground": "Latar Belakang Jendela",
"windowBackgroundDesc": "Atur warna latar belakang jendela aplikasi.",
"appMainView": "Tampilan Utama Aplikasi",
"appMainViewDesc": "Atur warna latar belakang area konten utama.",
"primary": "Utama",
"primaryDesc": "Atur warna utama untuk komponen UI.",
"accent": "Aksen",
"accentDesc": "Atur warna aksen untuk sorotan UI.",
"destructive": "Merusak",
"destructiveDesc": "Atur warna untuk tindakan yang merusak.",
"resetToDefault": "Setel Ulang ke Default",
"resetToDefaultDesc": "Setel ulang semua pengaturan tampilan ke default.",
"resetAppearanceSuccess": "Tampilan berhasil diatur ulang",
"resetAppearanceSuccessDesc": "Semua pengaturan tampilan telah dikembalikan ke default.",
"chatWidth": "Lebar Obrolan",
"chatWidthDesc": "Sesuaikan lebar tampilan obrolan.",
"codeBlockTitle": "Blok Kode",
"codeBlockDesc": "Pilih gaya penyorotan sintaks.",
"showLineNumbers": "Tampilkan Nomor Baris",
"showLineNumbersDesc": "Tampilkan nomor baris di blok kode.",
"resetCodeBlockStyle": "Setel Ulang Gaya Blok Kode",
"resetCodeBlockStyleDesc": "Setel ulang gaya blok kode ke default.",
"resetCodeBlockSuccess": "Gaya blok kode berhasil diatur ulang",
"resetCodeBlockSuccessDesc": "Gaya blok kode telah dikembalikan ke default."
},
"hardware": {
"os": "Sistem Operasi",
"name": "Nama",
"version": "Versi",
"cpu": "CPU",
"model": "Model",
"architecture": "Arsitektur",
"cores": "Inti",
"instructions": "Instruksi",
"usage": "Penggunaan",
"memory": "Memori",
"totalRam": "Total RAM",
"availableRam": "RAM Tersedia",
"vulkan": "Vulkan",
"enableVulkan": "Aktifkan Vulkan",
"enableVulkanDesc": "Gunakan API Vulkan untuk akselerasi GPU. Jangan aktifkan Vulkan jika Anda memiliki GPU NVIDIA karena dapat menyebabkan masalah kompatibilitas.",
"gpus": "GPU",
"noGpus": "Tidak ada GPU yang terdeteksi",
"vram": "VRAM",
"freeOf": "bebas dari",
"driverVersion": "Versi Driver",
"computeCapability": "Kemampuan Komputasi",
"systemMonitor": "Monitor Sistem"
},
"httpsProxy": {
"proxy": "Proksi",
"proxyUrl": "URL Proksi",
"proxyUrlDesc": "URL dan port server proksi Anda.",
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
"authentication": "Otentikasi",
"authenticationDesc": "Kredensial untuk server proksi, jika diperlukan.",
"username": "Nama Pengguna",
"password": "Kata Sandi",
"noProxy": "Tanpa Proksi",
"noProxyDesc": "Daftar host yang dipisahkan koma untuk melewati proksi.",
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
"sslVerification": "Verifikasi SSL",
"ignoreSsl": "Abaikan Sertifikat SSL",
"ignoreSslDesc": "Izinkan sertifikat yang ditandatangani sendiri atau tidak terverifikasi. Ini mungkin diperlukan untuk beberapa proksi tetapi mengurangi keamanan. Hanya aktifkan jika Anda mempercayai proksi Anda.",
"proxySsl": "Proksi SSL",
"proxySslDesc": "Validasi sertifikat SSL saat menghubungkan ke proksi.",
"proxyHostSsl": "Host Proksi SSL",
"proxyHostSslDesc": "Validasi sertifikat SSL dari host proksi.",
"peerSsl": "SSL Sejawat",
"peerSslDesc": "Validasi sertifikat SSL dari koneksi sejawat.",
"hostSsl": "Host SSL",
"hostSslDesc": "Validasi sertifikat SSL dari host tujuan."
},
"localApiServer": {
"title": "Server API Lokal",
"description": "Jalankan server yang kompatibel dengan OpenAI secara lokal.",
"startServer": "Mulai Server",
"stopServer": "Hentikan Server",
"serverLogs": "Log Server",
"serverLogsDesc": "Lihat log terperinci dari server API lokal.",
"openLogs": "Buka Log",
"serverConfiguration": "Konfigurasi Server",
"serverHost": "Host Server",
"serverHostDesc": "Alamat jaringan untuk server.",
"serverPort": "Port Server",
"serverPortDesc": "Nomor port untuk server API.",
"apiPrefix": "Prefiks API",
"apiPrefixDesc": "Prefiks jalur untuk titik akhir API.",
"apiKey": "Kunci API",
"apiKeyDesc": "Otentikasi permintaan dengan kunci API.",
"trustedHosts": "Host Tepercaya",
"trustedHostsDesc": "Host yang diizinkan untuk mengakses server, dipisahkan dengan koma.",
"advancedSettings": "Pengaturan Lanjutan",
"cors": "Berbagi Sumber Daya Lintas Asal (CORS)",
"corsDesc": "Izinkan permintaan lintas asal ke server API.",
"verboseLogs": "Log Server Verbose",
"verboseLogsDesc": "Aktifkan log server terperinci untuk debugging."
},
"privacy": {
"analytics": "Analitik",
"helpUsImprove": "Bantu kami meningkatkan",
"helpUsImproveDesc": "Untuk membantu kami meningkatkan Jan, Anda dapat membagikan data anonim seperti penggunaan fitur dan jumlah pengguna. Kami tidak pernah mengumpulkan obrolan atau informasi pribadi Anda.",
"privacyPolicy": "Anda memiliki kendali penuh atas data Anda. Pelajari lebih lanjut di Kebijakan Privasi kami.",
"analyticsDesc": "Untuk meningkatkan Jan, kami perlu memahami bagaimana ia digunakan—tetapi hanya dengan bantuan Anda. Anda dapat mengubah pengaturan ini kapan saja.",
"privacyPromises": "Pilihan Anda di sini tidak akan mengubah janji privasi inti kami:",
"promise1": "Percakapan Anda tetap pribadi dan di perangkat Anda",
"promise2": "Kami tidak pernah mengumpulkan informasi pribadi atau konten obrolan Anda",
"promise3": "Semua pembagian data bersifat anonim dan diagregasi",
"promise4": "Anda dapat memilih keluar kapan saja tanpa kehilangan fungsionalitas",
"promise5": "Kami transparan tentang apa yang kami kumpulkan dan mengapa"
},
"general": {
"showInFinder": "Tampilkan di Finder",
"showInFileExplorer": "Tampilkan di File Explorer",
"openContainingFolder": "Buka Folder Induk",
"failedToRelocateDataFolder": "Gagal memindahkan folder data",
"failedToRelocateDataFolderDesc": "Gagal memindahkan folder data. Silakan coba lagi.",
"devVersion": "Versi pengembangan terdeteksi",
"noUpdateAvailable": "Anda menjalankan versi terbaru",
"updateError": "Gagal memeriksa pembaruan",
"appVersion": "Versi Aplikasi",
"checkForUpdates": "Periksa Pembaruan",
"checkForUpdatesDesc": "Periksa apakah versi Jan yang lebih baru tersedia.",
"checkingForUpdates": "Memeriksa pembaruan...",
"copied": "Tersalin",
"copyPath": "Salin Jalur",
"changeLocation": "Ubah Lokasi",
"openLogs": "Buka Log",
"revealLogs": "Tampilkan Log",
"factoryResetTitle": "Setel Ulang ke Pengaturan Pabrik",
"factoryResetDesc": "Ini akan mengatur ulang semua pengaturan aplikasi ke default. Tindakan ini tidak dapat dibatalkan. Kami hanya merekomendasikan ini jika aplikasi rusak.",
"cancel": "Batal",
"reset": "Setel Ulang",
"resources": "Sumber Daya",
"documentation": "Dokumentasi",
"documentationDesc": "Pelajari cara menggunakan Jan dan jelajahi fitur-fiturnya.",
"viewDocs": "Lihat Dokumentasi",
"releaseNotes": "Catatan Rilis",
"releaseNotesDesc": "Lihat apa yang baru di versi terbaru Jan.",
"viewReleases": "Lihat Rilis",
"community": "Komunitas",
"github": "GitHub",
"githubDesc": "Berkontribusi pada pengembangan Jan.",
"discord": "Discord",
"discordDesc": "Bergabunglah dengan komunitas kami untuk dukungan dan diskusi.",
"support": "Dukungan",
"reportAnIssue": "Laporkan Masalah",
"reportAnIssueDesc": "Menemukan bug? Bantu kami dengan mengajukan masalah di GitHub.",
"reportIssue": "Laporkan Masalah",
"credits": "Kredit",
"creditsDesc1": "Jan dibuat dengan ❤️ oleh Tim Menlo.",
"creditsDesc2": "Terima kasih khusus kepada dependensi sumber terbuka kami—terutama llama.cpp dan Tauri—dan kepada komunitas AI kami yang luar biasa."
},
"extensions": {
"title": "Ekstensi"
},
"dialogs": {
"changeDataFolder": {
"title": "Ubah Lokasi Folder Data",
"description": "Apakah Anda yakin ingin mengubah lokasi folder data? Ini akan memindahkan semua data Anda ke lokasi baru dan memulai ulang aplikasi.",
"currentLocation": "Lokasi Saat Ini:",
"newLocation": "Lokasi Baru:",
"cancel": "Batal",
"changeLocation": "Ubah Lokasi"
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"welcome": "Selamat Datang di Jan",
"description": "Untuk memulai, Anda perlu mengunduh model AI lokal atau terhubung ke model cloud menggunakan kunci API",
"localModel": "Siapkan model lokal",
"remoteProvider": "Siapkan penyedia jarak jauh"
}

View File

@ -0,0 +1,28 @@
{
"title": "Monitor Sistem",
"cpuUsage": "Penggunaan CPU",
"model": "Model",
"cores": "Inti",
"architecture": "Arsitektur",
"currentUsage": "Penggunaan Saat Ini",
"memoryUsage": "Penggunaan Memori",
"totalRam": "Total RAM",
"availableRam": "RAM Tersedia",
"usedRam": "RAM Terpakai",
"runningModels": "Model yang Berjalan",
"noRunningModels": "Tidak ada model yang sedang berjalan",
"provider": "Penyedia",
"uptime": "Waktu Aktif",
"actions": "Tindakan",
"stop": "Hentikan",
"activeGpus": "GPU Aktif",
"noGpus": "Tidak ada GPU yang terdeteksi",
"noActiveGpus": "Tidak ada GPU aktif. Semua GPU saat ini dinonaktifkan.",
"vramUsage": "Penggunaan VRAM",
"driverVersion": "Versi Driver:",
"computeCapability": "Kemampuan Komputasi:",
"active": "Aktif",
"performance": "Kinerja",
"resources": "Sumber Daya",
"refresh": "Segarkan"
}

View File

@ -0,0 +1,11 @@
{
"title": "Permintaan Panggilan Alat",
"description": "Asisten ingin menggunakan alat: <strong>{{toolName}}</strong>",
"securityNotice": "<strong>Pemberitahuan Keamanan:</strong> Alat berbahaya atau konten percakapan berpotensi menipu asisten untuk mencoba tindakan berbahaya. Tinjau setiap panggilan alat dengan cermat sebelum menyetujui.",
"deny": "Tolak",
"allowOnce": "Izinkan Sekali",
"alwaysAllow": "Selalu Izinkan",
"permissions": "Izin",
"approve": "Setujui",
"reject": "Tolak"
}

View File

@ -0,0 +1,10 @@
{
"toolApproval": {
"title": "Persetujuan Alat Diperlukan",
"securityNotice": "Alat ini ingin melakukan suatu tindakan. Harap tinjau dan setujui.",
"deny": "Tolak",
"allowOnce": "Izinkan Sekali",
"alwaysAllow": "Selalu Izinkan",
"description": "Asisten ingin menggunakan <strong>{{toolName}}</strong>"
}
}

View File

@ -0,0 +1,10 @@
{
"newVersion": "Versi Baru {{version}}",
"updateAvailable": "Pembaruan Tersedia",
"nightlyBuild": "Build Malam",
"showReleaseNotes": "Tampilkan Catatan Rilis",
"hideReleaseNotes": "Sembunyikan Catatan Rilis",
"remindMeLater": "Ingatkan Saya Nanti",
"downloading": "Mengunduh...",
"updateNow": "Perbarui Sekarang"
}

View File

@ -0,0 +1,32 @@
{
"title": "Trợ lý",
"editAssistant": "Chỉnh sửa Trợ lý",
"deleteAssistant": "Xóa Trợ lý",
"deleteConfirmation": "Xóa Trợ lý",
"deleteConfirmationDesc": "Bạn có chắc chắn muốn xóa trợ lý này không? Hành động này không thể hoàn tác.",
"cancel": "Hủy",
"delete": "Xóa",
"addAssistant": "Thêm Trợ lý",
"emoji": "Biểu tượng",
"name": "Tên",
"enterName": "Nhập tên",
"description": "Mô tả (tùy chọn)",
"enterDescription": "Nhập mô tả",
"instructions": "Hướng dẫn",
"enterInstructions": "Nhập hướng dẫn",
"predefinedParameters": "Tham số được xác định trước",
"parameters": "Tham số",
"key": "Khóa",
"value": "Giá trị",
"stringValue": "Chuỗi",
"numberValue": "Số",
"booleanValue": "Boolean",
"jsonValue": "JSON",
"trueValue": "Đúng",
"falseValue": "Sai",
"jsonValuePlaceholder": "Giá trị JSON",
"save": "Lưu",
"createNew": "Tạo Trợ lý Mới",
"personality": "Tính cách",
"capabilities": "Khả năng"
}

View File

@ -1,9 +1,10 @@
{
"chat": {
"welcome": "Xin chào, bạn khỏe không?",
"description": "Tôi có thể giúp gì cho bạn hôm nay?",
"status": {
"empty": "Không tìm thấy cuộc trò chuyện"
}
}
}
"welcome": "Chào bạn, bạn khỏe không?",
"description": "Hôm nay tôi có thể giúp gì cho bạn?",
"status": {
"empty": "Không tìm thấy cuộc trò chuyện nào"
},
"sendMessage": "Gửi tin nhắn",
"newConversation": "Cuộc trò chuyện mới",
"clearHistory": "Xóa lịch sử"
}

View File

@ -1,31 +1,267 @@
{
"common": {
"general": "Chung",
"settings": "Cài đặt",
"modelProviders": "Nhà cung cấp mô hình",
"appearance": "Giao diện",
"privacy": "Quyền riêng tư",
"keyboardShortcuts": "Phím tắt",
"newChat": "Cuộc trò chuyện mới",
"favorites": "Yêu thích",
"recents": "Gần đây",
"hub": "Hub",
"helpSupport": "Trợ giúp & Hỗ trợ",
"unstarAll": "Bỏ đánh dấu tất cả",
"unstar": "Bỏ đánh dấu",
"deleteAll": "Xóa tất cả",
"star": "Đánh dấu sao",
"rename": "Đổi tên",
"delete": "Xóa",
"dataFolder": "Thư mục dữ liệu",
"others": "Khác",
"language": "Ngôn ngữ",
"reset": "Đặt lại",
"search": "Tìm kiếm",
"name": "Tên",
"placeholder": {
"chatInput": "Hỏi tôi bất cứ điều gì..."
"assistants": "Trợ lý",
"hardware": "Phần cứng",
"mcp-servers": "Máy chủ Mcp",
"local_api_server": "Máy chủ API cục bộ",
"https_proxy": "Proxy HTTPS",
"extensions": "Tiện ích mở rộng",
"general": "Chung",
"settings": "Cài đặt",
"modelProviders": "Nhà cung cấp Mô hình",
"appearance": "Giao diện",
"privacy": "Quyền riêng tư",
"keyboardShortcuts": "Phím tắt",
"newChat": "Trò chuyện Mới",
"favorites": "Yêu thích",
"recents": "Gần đây",
"hub": "Hub",
"helpSupport": "Trợ giúp & Hỗ trợ",
"helpUsImproveJan": "Giúp chúng tôi cải thiện Jan",
"unstarAll": "Bỏ gắn dấu sao tất cả",
"unstar": "Bỏ gắn dấu sao",
"deleteAll": "Xóa tất cả",
"star": "Gắn dấu sao",
"rename": "Đổi tên",
"delete": "Xóa",
"copied": "Đã sao chép!",
"dataFolder": "Thư mục Dữ liệu",
"others": "Khác",
"language": "Ngôn ngữ",
"reset": "Đặt lại",
"search": "Tìm kiếm",
"name": "Tên",
"cancel": "Hủy",
"create": "Tạo",
"save": "Lưu",
"edit": "Chỉnh sửa",
"copy": "Sao chép",
"back": "Quay lại",
"close": "Đóng",
"next": "Tiếp theo",
"finish": "Hoàn thành",
"skip": "Bỏ qua",
"allow": "Cho phép",
"deny": "Từ chối",
"start": "Bắt đầu",
"stop": "Dừng",
"preview": "Xem trước",
"compactWidth": "Chiều rộng nhỏ gọn",
"fullWidth": "Chiều rộng đầy đủ",
"dark": "Tối",
"light": "Sáng",
"system": "Hệ thống",
"auto": "Tự động",
"english": "Tiếng Anh",
"medium": "Trung bình",
"newThread": "Chủ đề mới",
"noResultsFound": "Không tìm thấy kết quả nào",
"noThreadsYet": "Chưa có chủ đề nào",
"noThreadsYetDesc": "Bắt đầu một cuộc trò chuyện mới để xem lịch sử chủ đề của bạn ở đây.",
"downloads": "Tải xuống",
"downloading": "Đang tải xuống",
"cancelDownload": "Hủy tải xuống",
"downloadCancelled": "Đã hủy tải xuống",
"downloadComplete": "Tải xuống hoàn tất",
"thinking": "Đang suy nghĩ...",
"thought": "Suy nghĩ",
"callingTool": "Đang gọi công cụ",
"completed": "Đã hoàn thành",
"image": "Hình ảnh",
"vision": "Thị giác",
"embeddings": "Nhúng",
"tools": "Công cụ",
"webSearch": "Tìm kiếm trên web",
"reasoning": "Lý luận",
"selectAModel": "Chọn một mô hình",
"noToolsAvailable": "Không có công cụ nào",
"noModelsFoundFor": "Không tìm thấy mô hình nào cho \"{{searchValue}}\"",
"customAvatar": "Ảnh đại diện tùy chỉnh",
"editAssistant": "Chỉnh sửa Trợ lý",
"jan": "Jan",
"metadata": "Siêu dữ liệu",
"regenerate": "Tái tạo",
"threadImage": "Hình ảnh chủ đề",
"editMessage": "Chỉnh sửa tin nhắn",
"deleteMessage": "Xóa tin nhắn",
"deleteThread": "Xóa chủ đề",
"renameThread": "Đổi tên chủ đề",
"threadTitle": "Tiêu đề chủ đề",
"deleteAllThreads": "Xóa tất cả các chủ đề",
"allThreadsUnfavorited": "Tất cả các chủ đề đã được bỏ yêu thích",
"deleteAllThreadsConfirm": "Bạn có chắc chắn muốn xóa tất cả các chủ đề không? Hành động này không thể hoàn tác.",
"addProvider": "Thêm nhà cung cấp",
"addOpenAIProvider": "Thêm nhà cung cấp OpenAI",
"enterNameForProvider": "Nhập tên cho nhà cung cấp của bạn",
"providerAlreadyExists": "Nhà cung cấp có tên \"{{name}}\" đã tồn tại. Vui lòng chọn một tên khác.",
"adjustFontSize": "Điều chỉnh kích thước phông chữ",
"changeLanguage": "Thay đổi ngôn ngữ",
"editTheme": "Chỉnh sửa chủ đề",
"editCodeBlockStyle": "Chỉnh sửa kiểu khối mã",
"editServerHost": "Chỉnh sửa máy chủ lưu trữ",
"pickColorWindowBackground": "Chọn màu nền cửa sổ",
"pickColorAppMainView": "Chọn màu chế độ xem chính của ứng dụng",
"pickColorAppPrimary": "Chọn màu chính của ứng dụng",
"pickColorAppAccent": "Chọn màu nhấn của ứng dụng",
"pickColorAppDestructive": "Chọn màu hủy của ứng dụng",
"apiKeyRequired": "Yêu cầu khóa API",
"enterTrustedHosts": "Nhập máy chủ đáng tin cậy",
"placeholder": {
"chatInput": "Hỏi tôi bất cứ điều gì..."
},
"confirm": "Xác nhận",
"loading": "Đang tải...",
"error": "Lỗi",
"success": "Thành công",
"warning": "Cảnh báo",
"noResultsFoundDesc": "Chúng tôi không thể tìm thấy bất kỳ cuộc trò chuyện nào phù hợp với tìm kiếm của bạn. Hãy thử một từ khóa khác.",
"searchModels": "Tìm kiếm mô hình...",
"searchStyles": "Tìm kiếm kiểu...",
"createAssistant": "Tạo trợ lý",
"enterApiKey": "Nhập khóa API",
"scrollToBottom": "Cuộn xuống dưới cùng",
"addModel": {
"title": "Thêm mô hình",
"modelId": "ID mô hình",
"enterModelId": "Nhập ID mô hình",
"addModel": "Thêm mô hình",
"description": "Thêm một mô hình mới vào nhà cung cấp",
"exploreModels": "Xem danh sách mô hình từ nhà cung cấp"
},
"mcpServers": {
"editServer": "Chỉnh sửa máy chủ",
"addServer": "Thêm máy chủ",
"serverName": "Tên máy chủ",
"enterServerName": "Nhập tên máy chủ",
"command": "Lệnh",
"enterCommand": "Nhập lệnh",
"arguments": "Đối số",
"argument": "Đối số {{index}}",
"envVars": "Biến môi trường",
"key": "Khóa",
"value": "Giá trị",
"save": "Lưu"
},
"deleteServer": {
"title": "Xóa máy chủ",
"delete": "Xóa"
},
"editJson": {
"errorParse": "Không thể phân tích cú pháp JSON",
"errorPaste": "Không thể dán JSON",
"errorFormat": "Định dạng JSON không hợp lệ",
"titleAll": "Chỉnh sửa tất cả cấu hình máy chủ",
"placeholder": "Nhập cấu hình JSON...",
"save": "Lưu"
},
"editModel": {
"title": "Chỉnh sửa mô hình: {{modelId}}",
"description": "Định cấu hình khả năng của mô hình bằng cách chuyển đổi các tùy chọn bên dưới.",
"capabilities": "Khả năng",
"tools": "Công cụ",
"vision": "Thị giác",
"embeddings": "Nhúng",
"notAvailable": "Chưa có"
},
"toolApproval": {
"title": "Yêu cầu quyền truy cập công cụ",
"description": "Trợ lý muốn sử dụng <strong>{{toolName}}</strong>",
"securityNotice": "Chỉ cho phép các công cụ bạn tin tưởng. Các công cụ có thể truy cập hệ thống và dữ liệu của bạn.",
"deny": "Từ chối",
"allowOnce": "Cho phép một lần",
"alwaysAllow": "Luôn cho phép"
},
"deleteModel": {
"title": "Xóa mô hình: {{modelId}}",
"description": "Bạn có chắc chắn muốn xóa mô hình này không? Hành động này không thể hoàn tác.",
"success": "Mô hình {{modelId}} đã bị xóa vĩnh viễn.",
"cancel": "Hủy",
"delete": "Xóa"
},
"deleteProvider": {
"title": "Xóa nhà cung cấp",
"description": "Xóa nhà cung cấp này và tất cả các mô hình của nó. Hành động này không thể hoàn tác.",
"success": "Nhà cung cấp {{provider}} đã bị xóa vĩnh viễn.",
"confirmTitle": "Xóa nhà cung cấp: {{provider}}",
"confirmDescription": "Bạn có chắc chắn muốn xóa nhà cung cấp này không? Hành động này không thể hoàn tác.",
"cancel": "Hủy",
"delete": "Xóa"
},
"modelSettings": {
"title": "Cài đặt mô hình - {{modelId}}",
"description": "Định cấu hình cài đặt mô hình để tối ưu hóa hiệu suất và hành vi."
},
"dialogs": {
"changeDataFolder": {
"title": "Thay đổi vị trí thư mục dữ liệu",
"description": "Bạn có chắc chắn muốn thay đổi vị trí thư mục dữ liệu không? Thao tác này sẽ di chuyển tất cả dữ liệu của bạn đến vị trí mới và khởi động lại ứng dụng.",
"currentLocation": "Vị trí hiện tại:",
"newLocation": "Vị trí mới:",
"cancel": "Hủy",
"changeLocation": "Thay đổi vị trí"
},
"deleteAllThreads": {
"title": "Xóa tất cả các chủ đề",
"description": "Tất cả các chủ đề sẽ bị xóa. Hành động này không thể hoàn tác."
},
"deleteThread": {
"description": "Bạn có chắc chắn muốn xóa cuộc trò chuyện này không? Hành động này không thể hoàn tác."
},
"editMessage": {
"title": "Chỉnh sửa tin nhắn"
},
"messageMetadata": {
"title": "Siêu dữ liệu tin nhắn"
}
},
"toast": {
"allThreadsUnfavorited": {
"title": "Tất cả các chủ đề đã được bỏ yêu thích",
"description": "Tất cả các chủ đề đã được xóa khỏi danh sách yêu thích của bạn."
},
"deleteAllThreads": {
"title": "Xóa tất cả các chủ đề",
"description": "Tất cả các chủ đề đã bị xóa vĩnh viễn."
},
"renameThread": {
"title": "Đổi tên chủ đề",
"description": "Tiêu đề chủ đề đã được đổi thành '{{title}}'"
},
"deleteThread": {
"title": "Xóa chủ đề",
"description": "Chủ đề này đã bị xóa vĩnh viễn."
},
"editMessage": {
"title": "Chỉnh sửa tin nhắn",
"description": "Tin nhắn đã được chỉnh sửa thành công. Vui lòng đợi mô hình phản hồi."
},
"appUpdateDownloaded": {
"title": "Đã tải xuống bản cập nhật ứng dụng",
"description": "Bản cập nhật ứng dụng đã được tải xuống thành công."
},
"appUpdateDownloadFailed": {
"title": "Tải xuống bản cập nhật ứng dụng thất bại",
"description": "Không thể tải xuống bản cập nhật ứng dụng. Vui lòng thử lại."
},
"downloadComplete": {
"title": "Tải xuống hoàn tất",
"description": "Mô hình {{modelId}} đã được tải xuống"
},
"downloadCancelled": {
"title": "Đã hủy tải xuống",
"description": "Quá trình tải xuống đã bị hủy"
}
},
"cortexFailureDialog": {
"title": "Cortex không khởi động được",
"description": "Cortex không khởi động được. Vui lòng kiểm tra log để biết thêm chi tiết.",
"contactSupport": "Liên hệ Hỗ trợ",
"restartJan": "Khởi động lại Jan"
},
"outOfContextError": {
"title": "Lỗi ngoài 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 đang đầy. Chúng ta có thể mở rộng cửa sổ bộ nhớ (gọi là kích thước ngữ cảnh) để nó nhớ nhiều hơn, nhưng 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, có nghĩa là nó sẽ quên một phần lịch sử trò chuyện để nhường chỗ cho 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,31 @@
{
"sortNewest": "Mới nhất",
"sortMostDownloaded": "Tải nhiều nhất",
"use": "Sử dụng",
"download": "Tải xuống",
"downloaded": "Đã tải xuống",
"loadingModels": "Đang tải mô hình...",
"noModels": "Không tìm thấy mô hình nào",
"by": "bởi",
"downloads": "Lượt tải xuống",
"variants": "Biến thể",
"showVariants": "Hiển thị biến thể",
"useModel": "Sử dụng mô hình này",
"downloadModel": "Tải xuống mô hình",
"searchPlaceholder": "Tìm kiếm các mô hình trên Hugging Face...",
"editTheme": "Chỉnh sửa chủ đề",
"joyride": {
"recommendedModelTitle": "Mô hình được đề xuất",
"recommendedModelContent": "Duyệt và tải xuống các mô hình AI mạnh mẽ từ nhiều nhà cung cấp khác nhau, tất cả ở cùng một nơi. Chúng tôi khuyên bạn nên bắt đầu với Jan-Nano - một mô hình được tối ưu hóa cho các khả năng gọi hàm, tích hợp công cụ và nghiên cứu. Nó lý tưởng để xây dựng các tác nhân AI tương tác.",
"downloadInProgressTitle": "Đang tải xuống",
"downloadInProgressContent": "Mô hình của bạn hiện đang được tải xuống. Theo dõi tiến trình tại đây - sau khi hoàn tất, nó sẽ sẵn sàng để sử dụng.",
"downloadModelTitle": "Tải xuống mô hình",
"downloadModelContent": "Nhấp vào nút Tải xuống để bắt đầu tải xuống mô hình.",
"back": "Quay lại",
"close": "Đóng",
"lastWithDownload": "Tải xuống",
"last": "Hoàn thành",
"next": "Tiếp theo",
"skip": "Bỏ qua"
}
}

View File

@ -0,0 +1,3 @@
{
"noLogs": "Không có nhật ký nào"
}

View File

@ -0,0 +1,43 @@
{
"editServer": "Chỉnh sửa máy chủ MCP",
"addServer": "Thêm máy chủ MCP",
"serverName": "Tên máy chủ",
"enterServerName": "Nhập tên máy chủ",
"command": "Lệnh",
"enterCommand": "Nhập lệnh (uvx hoặc npx)",
"arguments": "Đối số",
"argument": "Đối số {{index}}",
"envVars": "Biến môi trường",
"key": "Khóa",
"value": "Giá trị",
"save": "Lưu",
"status": "Trạng thái",
"connected": "Đã kết nối",
"disconnected": "Đã ngắt kết nối",
"deleteServer": {
"title": "Xóa máy chủ MCP",
"description": "Bạn có chắc chắn muốn xóa máy chủ MCP {{serverName}} không? Hành động này không thể hoàn tác.",
"delete": "Xóa"
},
"editJson": {
"title": "Chỉnh sửa JSON cho máy chủ MCP: {{serverName}}",
"titleAll": "Chỉnh sửa tất cả JSON của máy chủ MCP",
"placeholder": "Nhập cấu hình JSON",
"errorParse": "Không thể phân tích cú pháp dữ liệu ban đầu",
"errorPaste": "Định dạng JSON không hợp lệ trong nội dung đã dán",
"errorFormat": "Định dạng JSON không hợp lệ",
"save": "Lưu"
},
"checkParams": "Vui lòng kiểm tra các tham số theo hướng dẫn.",
"title": "Máy chủ MCP",
"experimental": "Thử nghiệm",
"editAllJson": "Chỉnh sửa tất cả JSON của máy chủ",
"findMore": "Tìm thêm máy chủ MCP tại",
"allowPermissions": "Cho phép tất cả các quyền của công cụ MCP",
"allowPermissionsDesc": "Khi được bật, tất cả các lệnh gọi công cụ MCP sẽ được tự động phê duyệt mà không hiển thị hộp thoại cấp phép.",
"noServers": "Không tìm thấy máy chủ MCP nào",
"args": "Đối số",
"env": "Môi trường",
"serverStatusActive": "Máy chủ {{serverKey}} đã được kích hoạt thành công",
"serverStatusInactive": "Máy chủ {{serverKey}} đã được hủy kích hoạt thành công"
}

View File

@ -0,0 +1,5 @@
{
"addProvider": "Thêm nhà cung cấp",
"addOpenAIProvider": "Thêm nhà cung cấp OpenAI",
"enterNameForProvider": "Nhập tên cho nhà cung cấp"
}

View File

@ -0,0 +1,68 @@
{
"joyride": {
"chooseProviderTitle": "Chọn một nhà cung cấp",
"chooseProviderContent": "Chọn nhà cung cấp bạn muốn sử dụng, đảm bảo bạn có quyền truy cập vào khóa API cho nhà cung cấp đó.",
"getApiKeyTitle": "Lấy khóa API của bạn",
"getApiKeyContent": "Đăng nhập vào bảng điều khiển của nhà cung cấp để tìm hoặc tạo khóa API của bạn.",
"insertApiKeyTitle": "Chèn khóa API của bạn",
"insertApiKeyContent": "Dán khóa API của bạn vào đây để kết nối và kích hoạt nhà cung cấp.",
"back": "Quay lại",
"close": "Đóng",
"last": "Hoàn thành",
"next": "Tiếp theo",
"skip": "Bỏ qua"
},
"refreshModelsError": "Nhà cung cấp phải có URL cơ sở và khóa API được định cấu hình để tìm nạp các mô hình.",
"refreshModelsSuccess": "Đã thêm {{count}} mô hình mới từ {{provider}}.",
"noNewModels": "Không tìm thấy mô hình mới nào. Tất cả các mô hình có sẵn đã được thêm vào.",
"refreshModelsFailed": "Không thể tìm nạp các mô hình từ {{provider}}. Vui lòng kiểm tra khóa API và URL cơ sở của bạn.",
"models": "Mô hình",
"refreshing": "Đang làm mới...",
"refresh": "Làm mới",
"import": "Nhập",
"importModelSuccess": "Mô hình {{provider}} đã được nhập thành công.",
"importModelError": "Không thể nhập mô hình:",
"stop": "Dừng",
"start": "Bắt đầu",
"noModelFound": "Không tìm thấy mô hình nào",
"noModelFoundDesc": "Các mô hình có sẵn sẽ được liệt kê ở đây. Nếu bạn chưa có bất kỳ mô hình nào, hãy truy cập Hub để tải xuống.",
"configuration": "Cấu hình",
"apiEndpoint": "Điểm cuối API",
"testConnection": "Kiểm tra kết nối",
"addModel": {
"title": "Thêm mô hình mới",
"description": "Thêm một mô hình mới vào nhà cung cấp {{provider}}.",
"modelId": "ID mô hình",
"enterModelId": "Nhập ID mô hình",
"exploreModels": "Xem danh sách mô hình từ {{provider}}",
"addModel": "Thêm mô hình"
},
"deleteModel": {
"title": "Xóa mô hình: {{modelId}}",
"description": "Bạn có chắc chắn muốn xóa mô hình này không? Hành động này không thể hoàn tác.",
"success": "Mô hình {{modelId}} đã bị xóa vĩnh viễn.",
"cancel": "Hủy",
"delete": "Xóa"
},
"deleteProvider": {
"title": "Xóa nhà cung cấp",
"description": "Xóa nhà cung cấp này và tất cả các mô hình của nó. Hành động này không thể hoàn tác.",
"success": "Nhà cung cấp {{provider}} đã bị xóa vĩnhviễn.",
"confirmTitle": "Xóa nhà cung cấp: {{provider}}",
"confirmDescription": "Bạn có chắc chắn muốn xóa nhà cung cấp này không? Hành động này không thể hoàn tác.",
"cancel": "Hủy",
"delete": "Xóa"
},
"editModel": {
"title": "Chỉnh sửa mô hình: {{modelId}}",
"description": "Định cấu hình khả năng của mô hình bằng cách chuyển đổi các tùy chọn bên dưới.",
"capabilities": "Khả năng",
"tools": "Công cụ",
"vision": "Thị giác",
"embeddings": "Nhúng",
"notAvailable": "Chưa có"
},
"addProvider": "Thêm nhà cung cấp",
"addOpenAIProvider": "Thêm nhà cung cấp OpenAI",
"enterNameForProvider": "Nhập tên cho nhà cung cấp"
}

View File

@ -1,19 +1,248 @@
{
"settings": {
"general": {
"autoDownload": "Tự động tải xuống các bản cập nhật mới"
},
"dataFolder": {
"appData": "Dữ liệu ứng dụng",
"appDataDesc": "Vị trí mặc định cho tin nhắn và dữ liệu người dùng khác",
"appLogs": "Nhật ký ứng dụng",
"appLogsDesc": "Vị trí mặc định cho nhật ký ứng dụng"
},
"others": {
"spellCheck": "Kiểm tra chính tả",
"spellCheckDesc": "Bật để kích hoạt kiểm tra chính tả trong khung chat.",
"resetFactory": "Khôi phục cài đặt gốc",
"resetFactoryDesc": "Khôi phục ứng dụng về trạng thái ban đầu, xóa tất cả các mô hình và lịch sử trò chuyện. Hành động này không thể hoàn tác và chỉ nên thực hiện nếu ứng dụng bị hỏng"
"autoDownload": "Tự động tải xuống các bản cập nhật mới",
"checkForUpdates": "Kiểm tra Cập nhật",
"checkForUpdatesDesc": "Kiểm tra xem có phiên bản Jan mới hơn không.",
"checkingForUpdates": "Đang kiểm tra cập nhật...",
"noUpdateAvailable": "Bạn đang chạy phiên bản mới nhất",
"devVersion": "Đã phát hiện phiên bản phát triển",
"updateError": "Không thể kiểm tra cập nhật",
"changeLocation": "Thay đổi Vị trí",
"copied": "Đã sao chép",
"copyPath": "Sao chép Đường dẫn",
"openLogs": "Mở Nhật ký",
"revealLogs": "Hiển thị Nhật ký",
"showInFinder": "Hiển thị trong Finder",
"showInFileExplorer": "Hiển thị trong File Explorer",
"openContainingFolder": "Mở Thư mục Chứa",
"failedToRelocateDataFolder": "Không thể di chuyển thư mục dữ liệu",
"failedToRelocateDataFolderDesc": "Không thể di chuyển thư mục dữ liệu. Vui lòng thử lại.",
"factoryResetTitle": "Đặt lại về Cài đặt Gốc",
"factoryResetDesc": "Điều này sẽ đặt lại tất cả cài đặt ứng dụng về mặc định. Không thể hoàn tác. Chúng tôi chỉ khuyến nghị điều này nếu ứng dụng bị hỏng.",
"cancel": "Hủy",
"reset": "Đặt lại",
"resources": "Tài nguyên",
"documentation": "Tài liệu",
"documentationDesc": "Tìm hiểu cách sử dụng Jan và khám phá các tính năng của nó.",
"viewDocs": "Xem Tài liệu",
"releaseNotes": "Ghi chú Phát hành",
"releaseNotesDesc": "Xem có gì mới trong phiên bản Jan mới nhất.",
"viewReleases": "Xem Phát hành",
"community": "Cộng đồng",
"github": "GitHub",
"githubDesc": "Đóng góp vào việc phát triển Jan.",
"discord": "Discord",
"discordDesc": "Tham gia cộng đồng của chúng tôi để được hỗ trợ và thảo luận.",
"support": "Hỗ trợ",
"reportAnIssue": "Báo cáo Vấn đề",
"reportAnIssueDesc": "Tìm thấy lỗi? Hãy giúp chúng tôi bằng cách báo cáo vấn đề trên GitHub.",
"reportIssue": "Báo cáo Vấn đề",
"credits": "Ghi công",
"creditsDesc1": "Jan được xây dựng với ❤️ bởi Đội Menlo.",
"creditsDesc2": "Cảm ơn đặc biệt đến các phụ thuộc mã nguồn mở của chúng tôi—đặc biệt là llama.cpp và Tauri—và đến cộng đồng AI tuyệt vời của chúng tôi.",
"appVersion": "Phiên bản Ứng dụng",
"dataFolder": {
"appData": "Dữ liệu ứng dụng",
"appDataDesc": "Vị trí mặc định cho tin nhắn và dữ liệu người dùng khác.",
"appLogs": "Nhật ký ứng dụng",
"appLogsDesc": "Xem nhật ký chi tiết của Ứng dụng."
},
"others": {
"spellCheck": "Kiểm tra chính tả",
"spellCheckDesc": "Bật kiểm tra chính tả cho các chủ đề của bạn.",
"resetFactory": "Đặt lại về cài đặt gốc",
"resetFactoryDesc": "Khôi phục ứng dụng về trạng thái ban đầu, xóa tất cả các mô hình và lịch sử trò chuyện. Hành động này không thể đảo ngược và chỉ được khuyến nghị nếu ứng dụng bị hỏng."
},
"shortcuts": {
"application": "Ứng dụng",
"newChat": "Trò chuyện mới",
"newChatDesc": "Tạo một cuộc trò chuyện mới.",
"toggleSidebar": "Bật/tắt thanh bên",
"toggleSidebarDesc": "Hiển thị hoặc ẩn thanh bên.",
"zoomIn": "Phóng to",
"zoomInDesc": "Tăng mức thu phóng.",
"zoomOut": "Thu nhỏ",
"zoomOutDesc": "Giảm mức thu phóng.",
"chat": "Trò chuyện",
"sendMessage": "Gửi tin nhắn",
"sendMessageDesc": "Gửi tin nhắn hiện tại.",
"enter": "Enter",
"newLine": "Dòng mới",
"newLineDesc": "Chèn một dòng mới.",
"shiftEnter": "Shift + Enter",
"navigation": "Điều hướng",
"goToSettings": "Đi tới Cài đặt",
"goToSettingsDesc": "Mở cài đặt."
},
"appearance": {
"title": "Giao diện",
"theme": "Chủ đề",
"themeDesc": "Khớp với chủ đề hệ điều hành.",
"fontSize": "Kích thước phông chữ",
"fontSizeDesc": "Điều chỉnh kích thước phông chữ của ứng dụng.",
"windowBackground": "Nền cửa sổ",
"windowBackgroundDesc": "Đặt màu nền cửa sổ ứng dụng.",
"appMainView": "Chế độ xem chính của ứng dụng",
"appMainViewDesc": "Đặt màu nền cho vùng nội dung chính.",
"primary": "Chính",
"primaryDesc": "Đặt màu chính cho các thành phần giao diện người dùng.",
"accent": "Nhấn",
"accentDesc": "Đặt màu nhấn cho các điểm nổi bật giao diện người dùng.",
"destructive": "Hủy",
"destructiveDesc": "Đặt màu cho các hành động hủy.",
"resetToDefault": "Đặt lại về mặc định",
"resetToDefaultDesc": "Đặt lại tất cả cài đặt giao diện về mặc định.",
"resetAppearanceSuccess": "Giao diện đã được đặt lại thành công",
"resetAppearanceSuccessDesc": "Tất cả cài đặt giao diện đã được khôi phục về mặc định.",
"chatWidth": "Chiều rộng trò chuyện",
"chatWidthDesc": "Tùy chỉnh chiều rộng của chế độ xem trò chuyện.",
"codeBlockTitle": "Khối mã",
"codeBlockDesc": "Chọn kiểu tô sáng cú pháp.",
"showLineNumbers": "Hiển thị số dòng",
"showLineNumbersDesc": "Hiển thị số dòng trong khối mã.",
"resetCodeBlockStyle": "Đặt lại kiểu khối mã",
"resetCodeBlockStyleDesc": "Đặt lại kiểu khối mã về mặc định.",
"resetCodeBlockSuccess": "Kiểu khối mã đã được đặt lại thành công",
"resetCodeBlockSuccessDesc": "Kiểu khối mã đã được khôi phục về mặc định."
},
"hardware": {
"os": "Hệ điều hành",
"name": "Tên",
"version": "Phiên bản",
"cpu": "CPU",
"model": "Mô hình",
"architecture": "Kiến trúc",
"cores": "Lõi",
"instructions": "Hướng dẫn",
"usage": "Sử dụng",
"memory": "Bộ nhớ",
"totalRam": "Tổng RAM",
"availableRam": "RAM khả dụng",
"vulkan": "Vulkan",
"enableVulkan": "Bật Vulkan",
"enableVulkanDesc": "Sử dụng API Vulkan để tăng tốc GPU. Không bật Vulkan nếu bạn có GPU NVIDIA vì có thể gây ra sự cố tương thích.",
"gpus": "GPU",
"noGpus": "Không phát hiện thấy GPU nào",
"vram": "VRAM",
"freeOf": "trống",
"driverVersion": "Phiên bản trình điều khiển",
"computeCapability": "Khả năng tính toán",
"systemMonitor": "Giám sát hệ thống"
},
"httpsProxy": {
"proxy": "Proxy",
"proxyUrl": "URL proxy",
"proxyUrlDesc": "URL và cổng của máy chủ proxy của bạn.",
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
"authentication": "Xác thực",
"authenticationDesc": "Thông tin đăng nhập cho máy chủ proxy, nếu được yêu cầu.",
"username": "Tên người dùng",
"password": "Mật khẩu",
"noProxy": "Không có proxy",
"noProxyDesc": "Danh sách các máy chủ được phân tách bằng dấu phẩy để bỏ qua proxy.",
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
"sslVerification": "Xác minh SSL",
"ignoreSsl": "Bỏ qua chứng chỉ SSL",
"ignoreSslDesc": "Cho phép các chứng chỉ tự ký hoặc chưa được xác minh. Điều này có thể được yêu cầu cho một số proxy nhưng làm giảm tính bảo mật. Chỉ bật nếu bạn tin tưởng proxy của mình.",
"proxySsl": "Proxy SSL",
"proxySslDesc": "Xác thực chứng chỉ SSL khi kết nối với proxy.",
"proxyHostSsl": "Máy chủ proxy SSL",
"proxyHostSslDesc": "Xác thực chứng chỉ SSL của máy chủ proxy.",
"peerSsl": "Ngang hàng SSL",
"peerSslDesc": "Xác thực chứng chỉ SSL của các kết nối ngang hàng.",
"hostSsl": "Máy chủ SSL",
"hostSslDesc": "Xác thực chứng chỉ SSL của các máy chủ đích."
},
"localApiServer": {
"title": "Máy chủ API cục bộ",
"description": "Chạy máy chủ tương thích với OpenAI cục bộ.",
"startServer": "Khởi động máy chủ",
"stopServer": "Dừng máy chủ",
"serverLogs": "Nhật ký máy chủ",
"serverLogsDesc": "Xem nhật ký chi tiết của máy chủ API cục bộ.",
"openLogs": "Mở nhật ký",
"serverConfiguration": "Cấu hình máy chủ",
"serverHost": "Máy chủ lưu trữ",
"serverHostDesc": "Địa chỉ mạng cho máy chủ.",
"serverPort": "Cổng máy chủ",
"serverPortDesc": "Số cổng cho máy chủ API.",
"apiPrefix": "Tiền tố API",
"apiPrefixDesc": "Tiền tố đường dẫn cho các điểm cuối API.",
"apiKey": "Khóa API",
"apiKeyDesc": "Xác thực các yêu cầu bằng khóa API.",
"trustedHosts": "Máy chủ đáng tin cậy",
"trustedHostsDesc": "Các máy chủ được phép truy cập máy chủ, được phân tách bằng dấu phẩy.",
"advancedSettings": "Cài đặt nâng cao",
"cors": "Chia sẻ tài nguyên giữa các nguồn gốc (CORS)",
"corsDesc": "Cho phép các yêu cầu cross-origin đến máy chủ API.",
"verboseLogs": "Nhật ký máy chủ chi tiết",
"verboseLogsDesc": "Bật nhật ký máy chủ chi tiết để gỡ lỗi."
},
"privacy": {
"analytics": "Phân tích",
"helpUsImprove": "Giúp chúng tôi cải thiện",
"helpUsImproveDesc": "Để giúp chúng tôi cải thiện Jan, bạn có thể chia sẻ dữ liệu ẩn danh như việc sử dụng tính năng và số lượng người dùng. Chúng tôi không bao giờ thu thập các cuộc trò chuyện hoặc thông tin cá nhân của bạn.",
"privacyPolicy": "Bạn có toàn quyền kiểm soát dữ liệu của mình. Tìm hiểu thêm trong Chính sách Quyền riêng tư của chúng tôi.",
"analyticsDesc": "Để cải thiện Jan, chúng tôi cần hiểu cách sử dụng—nhưng chỉ với sự giúp đỡ của bạn. Bạn có thể thay đổi cài đặt này bất cứ lúc nào.",
"privacyPromises": "Lựa chọn của bạn ở đây sẽ không thay đổi các cam kết quyền riêng tư cốt lõi của chúng tôi:",
"promise1": "Các cuộc trò chuyện của bạn vẫn riêng tư và trên thiết bị của bạn",
"promise2": "Chúng tôi không bao giờ thu thập thông tin cá nhân hoặc nội dung trò chuyện của bạn",
"promise3": "Tất cả việc chia sẻ dữ liệu đều ẩn danh và được tổng hợp",
"promise4": "Bạn có thể từ chối bất cứ lúc nào mà không mất chức năng",
"promise5": "Chúng tôi minh bạch về những gì chúng tôi thu thập và tại sao"
},
"general": {
"showInFinder": "Hiển thị trong Finder",
"showInFileExplorer": "Hiển thị trong File Explorer",
"openContainingFolder": "Mở Thư mục Chứa",
"failedToRelocateDataFolder": "Không thể di chuyển thư mục dữ liệu",
"failedToRelocateDataFolderDesc": "Không thể di chuyển thư mục dữ liệu. Vui lòng thử lại.",
"devVersion": "Đã phát hiện phiên bản phát triển",
"noUpdateAvailable": "Bạn đang chạy phiên bản mới nhất",
"updateError": "Không thể kiểm tra cập nhật",
"appVersion": "Phiên bản Ứng dụng",
"checkForUpdates": "Kiểm tra Cập nhật",
"checkForUpdatesDesc": "Kiểm tra xem có phiên bản Jan mới hơn không.",
"checkingForUpdates": "Đang kiểm tra cập nhật...",
"copied": "Đã sao chép",
"copyPath": "Sao chép Đường dẫn",
"changeLocation": "Thay đổi Vị trí",
"openLogs": "Mở Nhật ký",
"revealLogs": "Hiển thị Nhật ký",
"factoryResetTitle": "Đặt lại về Cài đặt Gốc",
"factoryResetDesc": "Điều này sẽ đặt lại tất cả cài đặt ứng dụng về mặc định. Không thể hoàn tác. Chúng tôi chỉ khuyến nghị điều này nếu ứng dụng bị hỏng.",
"cancel": "Hủy",
"reset": "Đặt lại",
"resources": "Tài nguyên",
"documentation": "Tài liệu",
"documentationDesc": "Tìm hiểu cách sử dụng Jan và khám phá các tính năng của nó.",
"viewDocs": "Xem Tài liệu",
"releaseNotes": "Ghi chú Phát hành",
"releaseNotesDesc": "Xem có gì mới trong phiên bản Jan mới nhất.",
"viewReleases": "Xem Phát hành",
"community": "Cộng đồng",
"github": "GitHub",
"githubDesc": "Đóng góp vào việc phát triển Jan.",
"discord": "Discord",
"discordDesc": "Tham gia cộng đồng của chúng tôi để được hỗ trợ và thảo luận.",
"support": "Hỗ trợ",
"reportAnIssue": "Báo cáo Vấn đề",
"reportAnIssueDesc": "Tìm thấy lỗi? Hãy giúp chúng tôi bằng cách báo cáo vấn đề trên GitHub.",
"reportIssue": "Báo cáo Vấn đề",
"credits": "Ghi công",
"creditsDesc1": "Jan được xây dựng với ❤️ bởi Đội Menlo.",
"creditsDesc2": "Cảm ơn đặc biệt đến các phụ thuộc mã nguồn mở của chúng tôi—đặc biệt là llama.cpp và Tauri—và đến cộng đồng AI tuyệt vời của chúng tôi."
},
"extensions": {
"title": "Tiện ích mở rộng"
},
"dialogs": {
"changeDataFolder": {
"title": "Thay đổi vị trí thư mục dữ liệu",
"description": "Bạn có chắc chắn muốn thay đổi vị trí thư mục dữ liệu không? Thao tác này sẽ di chuyển tất cả dữ liệu của bạn đến vị trí mới và khởi động lại ứng dụng.",
"currentLocation": "Vị trí hiện tại:",
"newLocation": "Vị trí mới:",
"cancel": "Hủy",
"changeLocation": "Thay đổi vị trí"
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"welcome": "Chào mừng đến với Jan",
"description": "Để bắt đầu, bạn cần tải xuống một mô hình AI cục bộ hoặc kết nối với một mô hình đám mây bằng khóa API",
"localModel": "Thiết lập mô hình cục bộ",
"remoteProvider": "Thiết lập nhà cung cấp từ xa"
}

View File

@ -0,0 +1,28 @@
{
"title": "Giám sát hệ thống",
"cpuUsage": "Sử dụng CPU",
"model": "Mô hình",
"cores": "Lõi",
"architecture": "Kiến trúc",
"currentUsage": "Sử dụng hiện tại",
"memoryUsage": "Sử dụng bộ nhớ",
"totalRam": "Tổng RAM",
"availableRam": "RAM khả dụng",
"usedRam": "RAM đã sử dụng",
"runningModels": "Các mô hình đang chạy",
"noRunningModels": "Hiện không có mô hình nào đang chạy",
"provider": "Nhà cung cấp",
"uptime": "Thời gian hoạt động",
"actions": "Hành động",
"stop": "Dừng",
"activeGpus": "GPU đang hoạt động",
"noGpus": "Không phát hiện thấy GPU nào",
"noActiveGpus": "Không có GPU nào đang hoạt động. Tất cả các GPU hiện đang bị tắt.",
"vramUsage": "Sử dụng VRAM",
"driverVersion": "Phiên bản trình điều khiển:",
"computeCapability": "Khả năng tính toán:",
"active": "Hoạt động",
"performance": "Hiệu suất",
"resources": "Tài nguyên",
"refresh": "Làm mới"
}

View File

@ -0,0 +1,11 @@
{
"title": "Yêu cầu gọi công cụ",
"description": "Trợ lý muốn sử dụng công cụ: <strong>{{toolName}}</strong>",
"securityNotice": "<strong>Thông báo bảo mật:</strong> Các công cụ độc hại hoặc nội dung cuộc trò chuyện có khả năng lừa trợ lý thực hiện các hành động có hại. Hãy xem xét kỹ từng lệnh gọi công cụ trước khi phê duyệt.",
"deny": "Từ chối",
"allowOnce": "Cho phép một lần",
"alwaysAllow": "Luôn cho phép",
"permissions": "Quyền",
"approve": "Phê duyệt",
"reject": "Từ chối"
}

View File

@ -0,0 +1,10 @@
{
"toolApproval": {
"title": "Yêu cầu phê duyệt công cụ",
"securityNotice": "Công cụ này muốn thực hiện một hành động. Vui lòng xem xét và phê duyệt.",
"deny": "Từ chối",
"allowOnce": "Cho phép một lần",
"alwaysAllow": "Luôn cho phép",
"description": "Trợ lý muốn sử dụng <strong>{{toolName}}</strong>"
}
}

View File

@ -0,0 +1,10 @@
{
"newVersion": "Phiên bản mới {{version}}",
"updateAvailable": "Có bản cập nhật",
"nightlyBuild": "Bản dựng hàng đêm",
"showReleaseNotes": "Hiển thị ghi chú phát hành",
"hideReleaseNotes": "Ẩn ghi chú phát hành",
"remindMeLater": "Nhắc tôi sau",
"downloading": "Đang tải xuống...",
"updateNow": "Cập nhật ngay"
}

View File

@ -0,0 +1,32 @@
{
"title": "助手",
"editAssistant": "编辑助手",
"deleteAssistant": "删除助手",
"deleteConfirmation": "删除助手",
"deleteConfirmationDesc": "您确定要删除此助手吗?此操作无法撤销。",
"cancel": "取消",
"delete": "删除",
"addAssistant": "添加助手",
"emoji": "表情符号",
"name": "名称",
"enterName": "输入名称",
"description": "描述(可选)",
"enterDescription": "输入描述",
"instructions": "说明",
"enterInstructions": "输入说明",
"predefinedParameters": "预定义参数",
"parameters": "参数",
"key": "键",
"value": "值",
"stringValue": "字符串",
"numberValue": "数字",
"booleanValue": "布尔值",
"jsonValue": "JSON",
"trueValue": "真",
"falseValue": "假",
"jsonValuePlaceholder": "JSON 值",
"save": "保存",
"createNew": "创建新助手",
"personality": "个性",
"capabilities": "能力"
}

View File

@ -0,0 +1,10 @@
{
"welcome": "嗨,你好吗?",
"description": "今天我能为您做些什么?",
"status": {
"empty": "未找到聊天"
},
"sendMessage": "发送消息",
"newConversation": "新对话",
"clearHistory": "清除历史记录"
}

View File

@ -0,0 +1,267 @@
{
"assistants": "助手",
"hardware": "硬件",
"mcp-servers": "MCP 服务",
"local_api_server": "本地 API 服务",
"https_proxy": "HTTPS 代理",
"extensions": "扩展",
"general": "通用",
"settings": "设置",
"modelProviders": "模型提供商",
"appearance": "外观",
"privacy": "隐私",
"keyboardShortcuts": "快捷键",
"newChat": "新建聊天",
"favorites": "收藏",
"recents": "最近",
"hub": "中心",
"helpSupport": "帮助与支持",
"helpUsImproveJan": "帮助我们改进 Jan",
"unstarAll": "取消所有收藏",
"unstar": "取消收藏",
"deleteAll": "全部删除",
"star": "收藏",
"rename": "重命名",
"delete": "删除",
"copied": "已复制!",
"dataFolder": "数据文件夹",
"others": "其他",
"language": "语言",
"reset": "重置",
"search": "搜索",
"name": "名称",
"cancel": "取消",
"create": "创建",
"save": "保存",
"edit": "编辑",
"copy": "复制",
"back": "返回",
"close": "关闭",
"next": "下一步",
"finish": "完成",
"skip": "跳过",
"allow": "允许",
"deny": "拒绝",
"start": "开始",
"stop": "停止",
"preview": "预览",
"compactWidth": "紧凑宽度",
"fullWidth": "全宽",
"dark": "深色",
"light": "浅色",
"system": "系统",
"auto": "自动",
"english": "英语",
"medium": "中等",
"newThread": "新会话",
"noResultsFound": "未找到结果",
"noThreadsYet": "暂无会话",
"noThreadsYetDesc": "开始新的对话以在此处查看您的会话历史记录。",
"downloads": "下载",
"downloading": "下载中",
"cancelDownload": "取消下载",
"downloadCancelled": "下载已取消",
"downloadComplete": "下载完成",
"thinking": "思考中...",
"thought": "思考",
"callingTool": "调用工具",
"completed": "已完成",
"image": "图片",
"vision": "视觉",
"embeddings": "嵌入",
"tools": "工具",
"webSearch": "网页搜索",
"reasoning": "推理",
"selectAModel": "选择一个模型",
"noToolsAvailable": "无可用工具",
"noModelsFoundFor": "未找到“{{searchValue}}”的模型",
"customAvatar": "自定义头像",
"editAssistant": "编辑助手",
"jan": "Jan",
"metadata": "元数据",
"regenerate": "重新生成",
"threadImage": "会话图片",
"editMessage": "编辑消息",
"deleteMessage": "删除消息",
"deleteThread": "删除会话",
"renameThread": "重命名会话",
"threadTitle": "会话标题",
"deleteAllThreads": "删除所有会话",
"allThreadsUnfavorited": "所有会话已取消收藏",
"deleteAllThreadsConfirm": "您确定要删除所有会话吗?此操作无法撤销。",
"addProvider": "添加提供商",
"addOpenAIProvider": "添加 OpenAI 提供商",
"enterNameForProvider": "为您的提供商输入一个名称",
"providerAlreadyExists": "名为“{{name}}”的提供商已存在。请选择其他名称。",
"adjustFontSize": "调整字号",
"changeLanguage": "更改语言",
"editTheme": "编辑主题",
"editCodeBlockStyle": "编辑代码块样式",
"editServerHost": "编辑服务器主机",
"pickColorWindowBackground": "选择窗口背景颜色",
"pickColorAppMainView": "选择应用主视图颜色",
"pickColorAppPrimary": "选择应用主颜色",
"pickColorAppAccent": "选择应用强调颜色",
"pickColorAppDestructive": "选择应用警示颜色",
"apiKeyRequired": "需要 API 密钥",
"enterTrustedHosts": "输入受信任的主机",
"placeholder": {
"chatInput": "随便问我什么..."
},
"confirm": "确认",
"loading": "加载中...",
"error": "错误",
"success": "成功",
"warning": "警告",
"noResultsFoundDesc": "我们找不到任何与您的搜索匹配的聊天。请尝试其他关键字。",
"searchModels": "搜索模型...",
"searchStyles": "搜索样式...",
"createAssistant": "创建助手",
"enterApiKey": "输入 API 密钥",
"scrollToBottom": "滚动到底部",
"addModel": {
"title": "添加模型",
"modelId": "模型 ID",
"enterModelId": "输入模型 ID",
"addModel": "添加模型",
"description": "向提供商添加新模型",
"exploreModels": "查看提供商的模型列表"
},
"mcpServers": {
"editServer": "编辑服务器",
"addServer": "添加服务器",
"serverName": "服务器名称",
"enterServerName": "输入服务器名称",
"command": "命令",
"enterCommand": "输入命令",
"arguments": "参数",
"argument": "参数 {{index}}",
"envVars": "环境变量",
"key": "键",
"value": "值",
"save": "保存"
},
"deleteServer": {
"title": "删除服务器",
"delete": "删除"
},
"editJson": {
"errorParse": "解析 JSON 失败",
"errorPaste": "粘贴 JSON 失败",
"errorFormat": "JSON 格式无效",
"titleAll": "编辑所有服务器配置",
"placeholder": "输入 JSON 配置...",
"save": "保存"
},
"editModel": {
"title": "编辑模型:{{modelId}}",
"description": "通过切换以下选项来配置模型功能。",
"capabilities": "功能",
"tools": "工具",
"vision": "视觉",
"embeddings": "嵌入",
"notAvailable": "尚不可用"
},
"toolApproval": {
"title": "工具权限请求",
"description": "助手想要使用 <strong>{{toolName}}</strong>",
"securityNotice": "只允许您信任的工具。工具可以访问您的系统和数据。",
"deny": "拒绝",
"allowOnce": "允许一次",
"alwaysAllow": "始终允许"
},
"deleteModel": {
"title": "删除模型:{{modelId}}",
"description": "您确定要删除此模型吗?此操作无法撤销。",
"success": "模型 {{modelId}} 已被永久删除。",
"cancel": "取消",
"delete": "删除"
},
"deleteProvider": {
"title": "删除提供商",
"description": "删除此提供商及其所有模型。此操作无法撤销。",
"success": "提供商 {{provider}} 已被永久删除。",
"confirmTitle": "删除提供商:{{provider}}",
"confirmDescription": "您确定要删除此提供商吗?此操作无法撤销。",
"cancel": "取消",
"delete": "删除"
},
"modelSettings": {
"title": "模型设置 - {{modelId}}",
"description": "配置模型设置以优化性能和行为。"
},
"dialogs": {
"changeDataFolder": {
"title": "更改数据文件夹位置",
"description": "您确定要更改数据文件夹位置吗?这将把您的所有数据移动到新位置并重新启动应用程序。",
"currentLocation": "当前位置:",
"newLocation": "新位置:",
"cancel": "取消",
"changeLocation": "更改位置"
},
"deleteAllThreads": {
"title": "删除所有会话",
"description": "所有会话将被删除。此操作无法撤销。"
},
"deleteThread": {
"description": "您确定要删除此对话吗?此操作无法撤销。"
},
"editMessage": {
"title": "编辑消息"
},
"messageMetadata": {
"title": "消息元数据"
}
},
"toast": {
"allThreadsUnfavorited": {
"title": "所有会话已取消收藏",
"description": "所有会话已从您的收藏中移除。"
},
"deleteAllThreads": {
"title": "删除所有会话",
"description": "所有会话已被永久删除。"
},
"renameThread": {
"title": "重命名会话",
"description": "会话标题已重命名为“{{title}}”"
},
"deleteThread": {
"title": "删除会话",
"description": "此会话已被永久删除。"
},
"editMessage": {
"title": "编辑消息",
"description": "消息编辑成功。请等待模型响应。"
},
"appUpdateDownloaded": {
"title": "应用更新已下载",
"description": "应用更新已成功下载。"
},
"appUpdateDownloadFailed": {
"title": "应用更新下载失败",
"description": "下载应用更新失败。请重试。"
},
"downloadComplete": {
"title": "下载完成",
"description": "模型 {{modelId}} 已下载"
},
"downloadCancelled": {
"title": "下载已取消",
"description": "下载过程已取消"
}
},
"cortexFailureDialog": {
"title": "Cortex 启动失败",
"description": "Cortex 启动失败。请检查日志以获取更多详细信息。",
"contactSupport": "联系支持",
"restartJan": "重启 Jan"
},
"outOfContextError": {
"title": "超出上下文错误",
"description": "此聊天正在达到AI的内存限制就像白板填满了一样。我们可以扩展内存窗口称为上下文大小使其记住更多内容但可能会使用更多计算机内存。我们也可以截断输入这意味着它会忘记一些聊天历史记录为新消息腾出空间。",
"increaseContextSizeDescription": "您想要增加上下文大小吗?",
"truncateInput": "截断输入",
"increaseContextSize": "增加上下文大小"
}
}

View File

@ -0,0 +1,31 @@
{
"sortNewest": "最新",
"sortMostDownloaded": "最多下载",
"use": "使用",
"download": "下载",
"downloaded": "已下载",
"loadingModels": "正在加载模型...",
"noModels": "未找到模型",
"by": "由",
"downloads": "下载",
"variants": "变体",
"showVariants": "显示变体",
"useModel": "使用此模型",
"downloadModel": "下载模型",
"searchPlaceholder": "在 Hugging Face 上搜索模型...",
"editTheme": "编辑主题",
"joyride": {
"recommendedModelTitle": "推荐模型",
"recommendedModelContent": "在一个地方浏览和下载来自不同提供商的强大 AI 模型。我们建议从 Jan-Nano 开始 - 这是一个针对函数调用、工具集成和研究功能进行优化的模型。它非常适合构建交互式 AI 代理。",
"downloadInProgressTitle": "下载进行中",
"downloadInProgressContent": "您的模型正在下载中。在此处跟踪进度 - 完成后即可使用。",
"downloadModelTitle": "下载模型",
"downloadModelContent": "单击“下载”按钮开始下载模型。",
"back": "返回",
"close": "关闭",
"lastWithDownload": "下载",
"last": "完成",
"next": "下一步",
"skip": "跳过"
}
}

View File

@ -0,0 +1,3 @@
{
"noLogs": "无可用日志"
}

View File

@ -0,0 +1,43 @@
{
"editServer": "编辑 MCP 服务器",
"addServer": "添加 MCP 服务器",
"serverName": "服务器名称",
"enterServerName": "输入服务器名称",
"command": "命令",
"enterCommand": "输入命令 (uvx 或 npx)",
"arguments": "参数",
"argument": "参数 {{index}}",
"envVars": "环境变量",
"key": "键",
"value": "值",
"save": "保存",
"status": "状态",
"connected": "已连接",
"disconnected": "已断开",
"deleteServer": {
"title": "删除 MCP 服务器",
"description": "您确定要删除 MCP 服务器 {{serverName}} 吗?此操作无法撤销。",
"delete": "删除"
},
"editJson": {
"title": "编辑 MCP 服务器的 JSON{{serverName}}",
"titleAll": "编辑所有 MCP 服务器的 JSON",
"placeholder": "输入 JSON 配置",
"errorParse": "解析初始数据失败",
"errorPaste": "粘贴内容中的 JSON 格式无效",
"errorFormat": "JSON 格式无效",
"save": "保存"
},
"checkParams": "请根据教程检查参数。",
"title": "MCP 服务器",
"experimental": "实验性",
"editAllJson": "编辑所有服务器的 JSON",
"findMore": "在以下位置查找更多 MCP 服务器",
"allowPermissions": "允许所有 MCP 工具权限",
"allowPermissionsDesc": "启用后,所有 MCP 工具调用都将自动批准,而不会显示权限对话框。",
"noServers": "未找到 MCP 服务器",
"args": "参数",
"env": "环境",
"serverStatusActive": "服务器 {{serverKey}} 激活成功",
"serverStatusInactive": "服务器 {{serverKey}} 停用成功"
}

View File

@ -0,0 +1,5 @@
{
"addProvider": "添加提供商",
"addOpenAIProvider": "添加 OpenAI 提供商",
"enterNameForProvider": "输入提供商名称"
}

View File

@ -0,0 +1,68 @@
{
"joyride": {
"chooseProviderTitle": "选择一个提供商",
"chooseProviderContent": "选择您要使用的提供商,确保您有权访问其 API 密钥。",
"getApiKeyTitle": "获取您的 API 密钥",
"getApiKeyContent": "登录提供商的仪表板以查找或生成您的 API 密钥。",
"insertApiKeyTitle": "插入您的 API 密钥",
"insertApiKeyContent": "在此处粘贴您的 API 密钥以连接并激活提供商。",
"back": "返回",
"close": "关闭",
"last": "完成",
"next": "下一步",
"skip": "跳过"
},
"refreshModelsError": "提供商必须配置基本 URL 和 API 密钥才能获取模型。",
"refreshModelsSuccess": "从 {{provider}} 添加了 {{count}} 个新模型。",
"noNewModels": "未找到新模型。所有可用模型均已添加。",
"refreshModelsFailed": "从 {{provider}} 获取模型失败。请检查您的 API 密钥和基本 URL。",
"models": "模型",
"refreshing": "刷新中...",
"refresh": "刷新",
"import": "导入",
"importModelSuccess": "模型 {{provider}} 已成功导入。",
"importModelError": "导入模型失败:",
"stop": "停止",
"start": "开始",
"noModelFound": "未找到模型",
"noModelFoundDesc": "可用模型将在此处列出。如果您还没有任何模型,请访问中心下载。",
"configuration": "配置",
"apiEndpoint": "API 端点",
"testConnection": "测试连接",
"addModel": {
"title": "添加新模型",
"description": "向 {{provider}} 提供商添加新模型。",
"modelId": "模型 ID",
"enterModelId": "输入模型 ID",
"exploreModels": "查看 {{provider}} 的模型列表",
"addModel": "添加模型"
},
"deleteModel": {
"title": "删除模型:{{modelId}}",
"description": "您确定要删除此模型吗?此操作无法撤销。",
"success": "模型 {{modelId}} 已被永久删除。",
"cancel": "取消",
"delete": "删除"
},
"deleteProvider": {
"title": "删除提供商",
"description": "删除此提供商及其所有模型。此操作无法撤销。",
"success": "提供商 {{provider}} 已被永久删除。",
"confirmTitle": "删除提供商:{{provider}}",
"confirmDescription": "您确定要删除此提供商吗?此操作无法撤销。",
"cancel": "取消",
"delete": "删除"
},
"editModel": {
"title": "编辑模型:{{modelId}}",
"description": "通过切换以下选项来配置模型功能。",
"capabilities": "功能",
"tools": "工具",
"vision": "视觉",
"embeddings": "嵌入",
"notAvailable": "尚不可用"
},
"addProvider": "添加提供商",
"addOpenAIProvider": "添加 OpenAI 提供商",
"enterNameForProvider": "输入提供商名称"
}

View File

@ -0,0 +1,248 @@
{
"autoDownload": "自动下载新更新",
"checkForUpdates": "检查更新",
"checkForUpdatesDesc": "检查是否有更新版本的 Jan 可用。",
"checkingForUpdates": "正在检查更新...",
"noUpdateAvailable": "您正在运行最新版本",
"devVersion": "检测到开发版本",
"updateError": "检查更新失败",
"changeLocation": "更改位置",
"copied": "已复制",
"copyPath": "复制路径",
"openLogs": "打开日志",
"revealLogs": "显示日志",
"showInFinder": "在 Finder 中显示",
"showInFileExplorer": "在文件资源管理器中显示",
"openContainingFolder": "打开包含文件夹",
"failedToRelocateDataFolder": "无法重新定位数据文件夹",
"failedToRelocateDataFolderDesc": "无法重新定位数据文件夹。请重试。",
"factoryResetTitle": "重置为出厂设置",
"factoryResetDesc": "这将重置所有应用设置为默认值。此操作无法撤销。我们只建议在应用损坏时使用。",
"cancel": "取消",
"reset": "重置",
"resources": "资源",
"documentation": "文档",
"documentationDesc": "了解如何使用 Jan 并探索其功能。",
"viewDocs": "查看文档",
"releaseNotes": "发布说明",
"releaseNotesDesc": "查看 Jan 最新版本的新功能。",
"viewReleases": "查看发布",
"community": "社区",
"github": "GitHub",
"githubDesc": "为 Jan 的开发做出贡献。",
"discord": "Discord",
"discordDesc": "加入我们的社区获取支持和讨论。",
"support": "支持",
"reportAnIssue": "报告问题",
"reportAnIssueDesc": "发现错误?通过在 GitHub 上提交问题来帮助我们。",
"reportIssue": "报告问题",
"credits": "致谢",
"creditsDesc1": "Jan 由 Menlo 团队用 ❤️ 构建。",
"creditsDesc2": "特别感谢我们的开源依赖项——尤其是 llama.cpp 和 Tauri——以及我们出色的 AI 社区。",
"appVersion": "应用版本",
"dataFolder": {
"appData": "应用数据",
"appDataDesc": "消息和其他用户数据的默认位置。",
"appLogs": "应用日志",
"appLogsDesc": "查看应用的详细日志。"
},
"others": {
"spellCheck": "拼写检查",
"spellCheckDesc": "为您的线程启用拼写检查。",
"resetFactory": "恢复出厂设置",
"resetFactoryDesc": "将应用程序恢复到其初始状态,清除所有模型和聊天记录。此操作不可逆,仅在应用程序损坏时建议使用。"
},
"shortcuts": {
"application": "应用程序",
"newChat": "新建聊天",
"newChatDesc": "创建一个新的聊天。",
"toggleSidebar": "切换侧边栏",
"toggleSidebarDesc": "显示或隐藏侧边栏。",
"zoomIn": "放大",
"zoomInDesc": "增加缩放级别。",
"zoomOut": "缩小",
"zoomOutDesc": "减小缩放级别。",
"chat": "聊天",
"sendMessage": "发送消息",
"sendMessageDesc": "发送当前消息。",
"enter": "回车",
"newLine": "换行",
"newLineDesc": "插入一个新行。",
"shiftEnter": "Shift + Enter",
"navigation": "导航",
"goToSettings": "转到设置",
"goToSettingsDesc": "打开设置。"
},
"appearance": {
"title": "外观",
"theme": "主题",
"themeDesc": "匹配操作系统主题。",
"fontSize": "字号",
"fontSizeDesc": "调整应用的字号。",
"windowBackground": "窗口背景",
"windowBackgroundDesc": "设置应用窗口的背景颜色。",
"appMainView": "应用主视图",
"appMainViewDesc": "设置主内容区域的背景颜色。",
"primary": "主要",
"primaryDesc": "设置 UI 组件的主要颜色。",
"accent": "强调",
"accentDesc": "设置 UI 亮点的强调颜色。",
"destructive": "警示",
"destructiveDesc": "设置警示操作的颜色。",
"resetToDefault": "重置为默认值",
"resetToDefaultDesc": "将所有外观设置重置为默认值。",
"resetAppearanceSuccess": "外观重置成功",
"resetAppearanceSuccessDesc": "所有外观设置已恢复为默认值。",
"chatWidth": "聊天宽度",
"chatWidthDesc": "自定义聊天视图的宽度。",
"codeBlockTitle": "代码块",
"codeBlockDesc": "选择语法高亮样式。",
"showLineNumbers": "显示行号",
"showLineNumbersDesc": "在代码块中显示行号。",
"resetCodeBlockStyle": "重置代码块样式",
"resetCodeBlockStyleDesc": "将代码块样式重置为默认值。",
"resetCodeBlockSuccess": "代码块样式重置成功",
"resetCodeBlockSuccessDesc": "代码块样式已恢复为默认值。"
},
"hardware": {
"os": "操作系统",
"name": "名称",
"version": "版本",
"cpu": "CPU",
"model": "模型",
"architecture": "架构",
"cores": "核心",
"instructions": "指令",
"usage": "使用情况",
"memory": "内存",
"totalRam": "总内存",
"availableRam": "可用内存",
"vulkan": "Vulkan",
"enableVulkan": "启用 Vulkan",
"enableVulkanDesc": "使用 Vulkan API 进行 GPU 加速。如果您有 NVIDIA GPU请勿启用 Vulkan因为它可能会导致兼容性问题。",
"gpus": "GPU",
"noGpus": "未检测到 GPU",
"vram": "VRAM",
"freeOf": "可用,共",
"driverVersion": "驱动程序版本",
"computeCapability": "计算能力",
"systemMonitor": "系统监视器"
},
"httpsProxy": {
"proxy": "代理",
"proxyUrl": "代理 URL",
"proxyUrlDesc": "您的代理服务器的 URL 和端口。",
"proxyUrlPlaceholder": "http://proxy.example.com:8080",
"authentication": "认证",
"authenticationDesc": "您的代理服务器的凭据(如果需要)。",
"username": "用户名",
"password": "密码",
"noProxy": "无代理",
"noProxyDesc": "应绕过代理的主机列表,以逗号分隔。",
"noProxyPlaceholder": "localhost,127.0.0.1,.local",
"sslVerification": "SSL 验证",
"ignoreSsl": "忽略 SSL 证书",
"ignoreSslDesc": "允许自签名或未验证的证书。某些代理可能需要此选项,但会降低安全性。仅在您信任您的代理服务器时启用。",
"proxySsl": "代理 SSL",
"proxySslDesc": "连接到代理服务器时验证 SSL 证书。",
"proxyHostSsl": "代理主机 SSL",
"proxyHostSslDesc": "验证代理服务器主机的 SSL 证书。",
"peerSsl": "对等 SSL",
"peerSslDesc": "验证对等连接的 SSL 证书。",
"hostSsl": "主机 SSL",
"hostSslDesc": "验证目标主机的 SSL 证书。"
},
"localApiServer": {
"title": "本地 API 服务器",
"description": "在本地运行与 OpenAI 兼容的服务器。",
"startServer": "启动服务器",
"stopServer": "停止服务器",
"serverLogs": "服务器日志",
"serverLogsDesc": "查看本地 API 服务器的详细日志。",
"openLogs": "打开日志",
"serverConfiguration": "服务器配置",
"serverHost": "服务器主机",
"serverHostDesc": "服务器的网络地址。",
"serverPort": "服务器端口",
"serverPortDesc": "API 服务器的端口号。",
"apiPrefix": "API 前缀",
"apiPrefixDesc": "API 端点的路径前缀。",
"apiKey": "API 密钥",
"apiKeyDesc": "使用 API 密钥验证请求。",
"trustedHosts": "受信任的主机",
"trustedHostsDesc": "允许访问服务器的主机,以逗号分隔。",
"advancedSettings": "高级设置",
"cors": "跨源资源共享 (CORS)",
"corsDesc": "允许跨源请求访问 API 服务器。",
"verboseLogs": "详细服务器日志",
"verboseLogsDesc": "启用详细服务器日志以进行调试。"
},
"privacy": {
"analytics": "分析",
"helpUsImprove": "帮助我们改进",
"helpUsImproveDesc": "为了帮助我们改进 Jan您可以共享匿名数据如功能使用情况和用户数量。我们绝不会收集您的聊天记录或个人信息。",
"privacyPolicy": "您完全控制您的数据。在我们的隐私政策中了解更多信息。",
"analyticsDesc": "为了改进 Jan我们需要了解它的使用方式——但这需要您的帮助。您可以随时更改此设置。",
"privacyPromises": "您在此处的选择不会改变我们的核心隐私承诺:",
"promise1": "您的对话保持私密并保留在您的设备上",
"promise2": "我们绝不会收集您的个人信息或聊天内容",
"promise3": "所有数据共享都是匿名和聚合的",
"promise4": "您可以随时选择退出而不会丢失功能",
"promise5": "我们对收集的内容和原因保持透明"
},
"general": {
"showInFinder": "在 Finder 中显示",
"showInFileExplorer": "在文件资源管理器中显示",
"openContainingFolder": "打开包含文件夹",
"failedToRelocateDataFolder": "无法重新定位数据文件夹",
"failedToRelocateDataFolderDesc": "无法重新定位数据文件夹。请重试。",
"devVersion": "检测到开发版本",
"noUpdateAvailable": "您正在运行最新版本",
"updateError": "检查更新失败",
"appVersion": "应用版本",
"checkForUpdates": "检查更新",
"checkForUpdatesDesc": "检查是否有更新版本的 Jan 可用。",
"checkingForUpdates": "正在检查更新...",
"copied": "已复制",
"copyPath": "复制路径",
"changeLocation": "更改位置",
"openLogs": "打开日志",
"revealLogs": "显示日志",
"factoryResetTitle": "重置为出厂设置",
"factoryResetDesc": "这将重置所有应用设置为默认值。此操作无法撤销。我们只建议在应用损坏时使用。",
"cancel": "取消",
"reset": "重置",
"resources": "资源",
"documentation": "文档",
"documentationDesc": "了解如何使用 Jan 并探索其功能。",
"viewDocs": "查看文档",
"releaseNotes": "发布说明",
"releaseNotesDesc": "查看 Jan 最新版本的新功能。",
"viewReleases": "查看发布",
"community": "社区",
"github": "GitHub",
"githubDesc": "为 Jan 的开发做出贡献。",
"discord": "Discord",
"discordDesc": "加入我们的社区获取支持和讨论。",
"support": "支持",
"reportAnIssue": "报告问题",
"reportAnIssueDesc": "发现错误?通过在 GitHub 上提交问题来帮助我们。",
"reportIssue": "报告问题",
"credits": "致谢",
"creditsDesc1": "Jan 由 Menlo 团队用 ❤️ 构建。",
"creditsDesc2": "特别感谢我们的开源依赖项——尤其是 llama.cpp 和 Tauri——以及我们出色的 AI 社区。"
},
"extensions": {
"title": "扩展"
},
"dialogs": {
"changeDataFolder": {
"title": "更改数据文件夹位置",
"description": "您确定要更改数据文件夹位置吗?这将把您的所有数据移动到新位置并重新启动应用程序。",
"currentLocation": "当前位置:",
"newLocation": "新位置:",
"cancel": "取消",
"changeLocation": "更改位置"
}
}
}

View File

@ -0,0 +1,6 @@
{
"welcome": "欢迎来到 Jan",
"description": "要开始使用,您需要下载本地 AI 模型或使用 API 密钥连接到云模型",
"localModel": "设置本地模型",
"remoteProvider": "设置远程提供商"
}

View File

@ -0,0 +1,28 @@
{
"title": "系统监视器",
"cpuUsage": "CPU 使用率",
"model": "模型",
"cores": "核心",
"architecture": "架构",
"currentUsage": "当前使用率",
"memoryUsage": "内存使用率",
"totalRam": "总内存",
"availableRam": "可用内存",
"usedRam": "已用内存",
"runningModels": "正在运行的模型",
"noRunningModels": "当前没有正在运行的模型",
"provider": "提供商",
"uptime": "正常运行时间",
"actions": "操作",
"stop": "停止",
"activeGpus": "活动 GPU",
"noGpus": "未检测到 GPU",
"noActiveGpus": "没有活动的 GPU。所有 GPU 当前都已禁用。",
"vramUsage": "VRAM 使用率",
"driverVersion": "驱动程序版本:",
"computeCapability": "计算能力:",
"active": "活动",
"performance": "性能",
"resources": "资源",
"refresh": "刷新"
}

View File

@ -0,0 +1,11 @@
{
"title": "工具调用请求",
"description": "助手想要使用工具:<strong>{{toolName}}</strong>",
"securityNotice": "<strong>安全警告:</strong>恶意的工具或对话内容可能会诱使助手尝试有害操作。在批准之前,请仔细审查每个工具调用。",
"deny": "拒绝",
"allowOnce": "允许一次",
"alwaysAllow": "始终允许",
"permissions": "权限",
"approve": "批准",
"reject": "拒绝"
}

View File

@ -0,0 +1,10 @@
{
"toolApproval": {
"title": "需要工具批准",
"securityNotice": "此工具想要执行一个操作。请审查并批准。",
"deny": "拒绝",
"allowOnce": "允许一次",
"alwaysAllow": "始终允许",
"description": "助手想要使用 <strong>{{toolName}}</strong>"
}
}

Some files were not shown because too many files have changed in this diff Show More