@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
596 lines • 21.5 kB
JavaScript
/**
* 翻訳クライアント - 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