UNPKG

@gftdcojp/gftd-orm

Version:

Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture

596 lines 21.5 kB
/** * 翻訳クライアント - Weblate統合による多言語対応 */ import { AuditLogManager, AuditEventType, AuditLogLevel } from './types'; import { log } from './utils/logger'; /** * 最小限の権限チェック機能(内部実装) * @deprecated auth0-integrationから移行 */ class SimplePermissionChecker { checkAuth0Permission(user, permission) { if (!user) return false; // 管理者は全権限を持つ if (user.role === 'service_role' || user.metadata?.roles?.includes('admin')) { return true; } // 特定の権限をチェック const permissions = user.metadata?.permissions || []; return permissions.includes(permission); } } /** * 翻訳クライアント */ export class TranslatorClient { constructor(config) { this.permissionChecker = new SimplePermissionChecker(); this.currentUser = null; this.cache = new Map(); this.config = config; log.info('Translator client initialized'); } /** * シングルトンインスタンスを取得 */ static getInstance(config) { if (!TranslatorClient.instance) { if (!config) { throw new Error('TranslatorConfig is required for first initialization'); } TranslatorClient.instance = new TranslatorClient(config); } else if (config) { // 設定が提供された場合は更新 TranslatorClient.instance.updateConfig(config); } return TranslatorClient.instance; } /** * 設定を更新 */ updateConfig(config) { this.config = { ...this.config, ...config }; // キャッシュをクリア(設定変更時) this.clearCache(); log.info('Translator configuration updated'); } /** * 現在のユーザーを設定 */ setCurrentUser(user) { this.currentUser = user; log.info(`Current user set: ${user.sub}`); } /** * APIリクエストヘルパー */ async apiRequest(endpoint, options = {}) { // キャッシュチェック const cacheKey = `${endpoint}:${JSON.stringify(options)}`; if (this.config.cache?.enabled && options.method === 'GET') { const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < (this.config.cache.ttl * 1000)) { return cached.data; } } const url = `${this.config.apiUrl}${endpoint}`; // ヘッダーを構築(APIキーがある場合のみ認証ヘッダーを追加) const headers = { 'Content-Type': 'application/json', ...(options.headers || {}), }; // APIキーがある場合のみAuthorizationヘッダーを追加 if (this.config.apiKey && this.config.apiKey.trim() !== '') { headers['Authorization'] = `Token ${this.config.apiKey}`; } const response = await fetch(url, { ...options, headers, }); if (!response.ok) { const error = await response.text(); // 401エラーの場合は認証が必要な操作であることを明示 if (response.status === 401) { throw new Error(`Weblate API authentication required: This operation requires a valid API key. Please set GFTD_WEBLATE_API_KEY environment variable.`); } throw new Error(`Weblate API error (${response.status}): ${error}`); } const data = await response.json(); // キャッシュに保存 if (this.config.cache?.enabled && (options.method === 'GET' || !options.method)) { this.cache.set(cacheKey, { data, timestamp: Date.now(), }); } return data; } /** * キャッシュをクリア */ clearCache() { this.cache.clear(); log.info('Translation cache cleared'); } /** * テナント固有のプロジェクトスラッグを取得 */ getTenantProjectSlug() { if (!this.config.tenantSpecific || !this.currentUser?.tenant_id) { return this.config.projectSlug; } return `${this.config.projectSlug}-${this.currentUser.tenant_id}`; } /** * プロジェクト一覧を取得 */ async getProjects() { try { const response = await this.apiRequest('/projects/'); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_READ, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: 'Translation projects fetched', details: { projectCount: response.results.length }, }); return response.results; } catch (error) { log.error(`Failed to get translation projects: ${error}`); throw error; } } /** * 特定のプロジェクトを取得 */ async getProject(projectSlug) { const slug = projectSlug || this.getTenantProjectSlug(); try { const project = await this.apiRequest(`/projects/${slug}/`); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_READ, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation project fetched: ${slug}`, details: { projectSlug: slug, sourceLanguage: project.source_language }, }); return project; } catch (error) { log.error(`Failed to get translation project: ${error}`); throw error; } } /** * コンポーネント一覧を取得 */ async getComponents(projectSlug) { const slug = projectSlug || this.getTenantProjectSlug(); try { const response = await this.apiRequest(`/projects/${slug}/components/`); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_READ, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation components fetched for project: ${slug}`, details: { projectSlug: slug, componentCount: response.results.length }, }); return response.results; } catch (error) { log.error(`Failed to get translation components: ${error}`); throw error; } } /** * 特定のコンポーネントを取得 */ async getComponent(componentSlug, projectSlug) { const pSlug = projectSlug || this.getTenantProjectSlug(); const cSlug = componentSlug || this.config.componentSlug; try { const component = await this.apiRequest(`/projects/${pSlug}/components/${cSlug}/`); return component; } catch (error) { log.error(`Failed to get translation component: ${error}`); throw error; } } /** * 翻訳言語一覧を取得 */ async getLanguages(projectSlug) { const slug = projectSlug || this.getTenantProjectSlug(); try { const response = await this.apiRequest(`/projects/${slug}/languages/`); return response.results; } catch (error) { log.error(`Failed to get translation languages: ${error}`); throw error; } } /** * 翻訳項目を取得 */ async getTranslations(language, options = {}) { const { projectSlug = this.getTenantProjectSlug(), componentSlug = this.config.componentSlug, page = 1, limit = 100, search, state, } = options; const params = new URLSearchParams({ page: page.toString(), page_size: limit.toString(), }); if (search) params.append('search', search); if (state) params.append('state', state); try { const response = await this.apiRequest(`/projects/${projectSlug}/components/${componentSlug}/translations/${language}/units/?${params.toString()}`); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_READ, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation items fetched for language: ${language}`, details: { projectSlug, componentSlug, language, count: response.count, page, limit }, }); return response; } catch (error) { log.error(`Failed to get translations: ${error}`); throw error; } } /** * 翻訳項目を更新 */ async updateTranslation(translationId, target, language, options = {}) { // 書き込み操作のためAPIキーが必要 this.checkApiKeyRequired(); const { projectSlug = this.getTenantProjectSlug(), componentSlug = this.config.componentSlug, state, } = options; const body = { target }; if (state) body.state = state; try { const updatedItem = await this.apiRequest(`/projects/${projectSlug}/components/${componentSlug}/translations/${language}/units/${translationId}/`, { method: 'PATCH', body: JSON.stringify(body), }); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_WRITE, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation updated: ${translationId}`, details: { projectSlug, componentSlug, language, translationId, target, state }, }); return updatedItem; } catch (error) { log.error(`Failed to update translation: ${error}`); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.ERROR, eventType: AuditEventType.DATA_WRITE, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'FAILURE', message: `Translation update failed: ${translationId}`, details: { projectSlug, componentSlug, language, translationId, error: String(error) }, }); throw error; } } /** * 翻訳統計を取得 */ async getTranslationStats(language, options = {}) { const { projectSlug = this.getTenantProjectSlug(), componentSlug = this.config.componentSlug, } = options; try { const stats = await this.apiRequest(`/projects/${projectSlug}/components/${componentSlug}/translations/${language}/statistics/`); return stats; } catch (error) { log.error(`Failed to get translation stats: ${error}`); throw error; } } /** * 翻訳項目を検索 */ async searchTranslations(query, options = {}) { const { language = this.config.defaultLanguage, projectSlug = this.getTenantProjectSlug(), componentSlug = this.config.componentSlug, page = 1, limit = 100, } = options; const params = new URLSearchParams({ search: query, page: page.toString(), page_size: limit.toString(), }); try { const response = await this.apiRequest(`/projects/${projectSlug}/components/${componentSlug}/translations/${language}/units/?${params.toString()}`); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_READ, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation search performed: ${query}`, details: { projectSlug, componentSlug, language, query, count: response.count, page, limit }, }); return response; } catch (error) { log.error(`Failed to search translations: ${error}`); throw error; } } /** * 翻訳プロジェクトを作成 */ async createProject(project) { // 書き込み操作のためAPIキーが必要 this.checkApiKeyRequired(); try { const createdProject = await this.apiRequest('/projects/', { method: 'POST', body: JSON.stringify(project), }); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_WRITE, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation project created: ${project.slug}`, details: { projectSlug: project.slug, name: project.name, sourceLanguage: project.source_language }, }); return createdProject; } catch (error) { log.error(`Failed to create translation project: ${error}`); throw error; } } /** * 翻訳コンポーネントを作成 */ async createComponent(projectSlug, component) { // 書き込み操作のためAPIキーが必要 this.checkApiKeyRequired(); try { const createdComponent = await this.apiRequest(`/projects/${projectSlug}/components/`, { method: 'POST', body: JSON.stringify(component), }); // 監査ログ記録 AuditLogManager.log({ level: AuditLogLevel.INFO, eventType: AuditEventType.DATA_WRITE, userId: this.currentUser?.sub, tenantId: this.currentUser?.tenant_id, result: 'SUCCESS', message: `Translation component created: ${component.slug}`, details: { projectSlug, componentSlug: component.slug, name: component.name, fileFormat: component.file_format }, }); return createdComponent; } catch (error) { log.error(`Failed to create translation component: ${error}`); throw error; } } /** * 権限をチェック */ checkPermission(permission) { if (!this.currentUser) { return false; } // APIキーが設定されている場合は許可 if (this.config.apiKey) { return true; } // ユーザー権限をチェック if (this.permissionChecker.checkAuth0Permission(this.currentUser, permission)) { return true; } return false; } /** * APIキーが必要な操作をチェック */ checkApiKeyRequired() { if (!this.config.apiKey || this.config.apiKey.trim() === '') { throw new Error('API key required: Write operations require a valid Weblate API key. Please set GFTD_WEBLATE_API_KEY environment variable.'); } } /** * 翻訳権限をチェック */ checkTranslationPermission(action) { const permissions = { read: 'translation:read', write: 'translation:write', manage: 'translation:manage', }; return this.checkPermission(permissions[action]); } /** * テナント固有の翻訳プロジェクトを初期化 */ async initializeTenantProject(tenantId) { const projectSlug = `${this.config.projectSlug}-${tenantId}`; try { // 既存のプロジェクトを確認 try { const existingProject = await this.getProject(projectSlug); return existingProject; } catch (error) { // プロジェクトが存在しない場合は作成 log.info(`Creating new translation project for tenant: ${tenantId}`); } // 新しいプロジェクトを作成 const project = await this.createProject({ name: `${this.config.projectSlug} - ${tenantId}`, slug: projectSlug, source_language: this.config.defaultLanguage, web: `https://your-app.com/tenant/${tenantId}`, instructions: `Translation project for tenant: ${tenantId}`, set_language_team: true, use_shared_tm: true, contribute_shared_tm: false, access_control: 1, // Private project translation_review: true, source_review: false, enable_hooks: true, }); log.info(`Translation project initialized for tenant: ${tenantId}`); return project; } catch (error) { log.error(`Failed to initialize tenant project: ${error}`); throw error; } } } /** * 翻訳設定ヘルパー */ export const translatorConfig = { /** * 環境変数から設定を構築 */ fromEnv() { return { apiUrl: process.env.GFTD_WEBLATE_API_URL || 'https://weblate-gftd-ai.fly.dev/api', apiKey: process.env.GFTD_WEBLATE_API_KEY || '', projectSlug: process.env.GFTD_WEBLATE_PROJECT_SLUG || 'scap-gftd-ai', componentSlug: process.env.GFTD_WEBLATE_COMPONENT_SLUG || 'messages', defaultLanguage: process.env.GFTD_WEBLATE_DEFAULT_LANGUAGE || 'en', supportedLanguages: (process.env.GFTD_WEBLATE_SUPPORTED_LANGUAGES || 'en,ja,zh-CN,es,fr,de,it,ko,pt,ru').split(','), cache: { enabled: process.env.GFTD_WEBLATE_CACHE_ENABLED !== 'false', // デフォルトで有効 ttl: parseInt(process.env.GFTD_WEBLATE_CACHE_TTL || '3600', 10), }, tenantSpecific: process.env.GFTD_WEBLATE_TENANT_SPECIFIC === 'true', }; }, /** * 開発環境用の設定 */ development() { return { apiUrl: 'https://weblate-gftd-ai.fly.dev/api', apiKey: '', // 開発環境では読み込み専用でAPIキー不要 projectSlug: 'scap-gftd-ai', componentSlug: 'messages', defaultLanguage: 'en', supportedLanguages: ['en', 'ja'], cache: { enabled: false, ttl: 300, }, tenantSpecific: false, }; }, /** * 本番環境用の設定 */ production() { return { apiUrl: process.env.GFTD_WEBLATE_API_URL || 'https://weblate-gftd-ai.fly.dev/api', apiKey: process.env.GFTD_WEBLATE_API_KEY || '', projectSlug: process.env.GFTD_WEBLATE_PROJECT_SLUG || 'scap-gftd-ai', componentSlug: process.env.GFTD_WEBLATE_COMPONENT_SLUG || 'messages', defaultLanguage: 'en', supportedLanguages: ['en', 'ja', 'zh-CN', 'es', 'fr', 'de', 'it', 'ko', 'pt', 'ru'], cache: { enabled: true, ttl: 3600, }, tenantSpecific: false, // 公開Weblateではテナント固有は無効 }; }, }; /** * 翻訳クライアントのヘルパー関数 */ export const translator = { /** * クライアントインスタンスを取得 */ client: (config) => TranslatorClient.getInstance(config), /** * 設定を取得 */ config: translatorConfig, /** * 翻訳機能を初期化 */ initialize: (config) => { return TranslatorClient.getInstance(config); }, /** * 現在のユーザーを設定 */ setUser: (user) => { const client = TranslatorClient.getInstance(); client.setCurrentUser(user); }, }; //# sourceMappingURL=translator-client.js.map