@gftdcojp/gftd-orm
Version:
Enterprise-grade real-time data platform with ksqlDB, inspired by Supabase architecture
492 lines • 17.6 kB
JavaScript
/**
* 翻訳機能用Reactフック
*/
import { useEffect, useState, useCallback, useMemo } from 'react';
// @todo [P2] Import from actual Weblate SDK when available
// Assigned: Team Lead | Milestone: Q1 2025 | Impact: Translation features
// import { useTranslation as useWeblateTranslation, useLanguage as useWeblateLanguage } from '@gftdcojp/weblate-nextjs-sdk';
import { TranslatorClient, translatorConfig } from '../translator-client';
import { useGftdOrm } from './useGftdOrm';
import { log } from '../utils/logger';
/**
* 翻訳機能フック
*/
export function useTranslator(options = {}) {
const { language: initialLanguage, namespace = 'default', tenantSpecific = true, autoRefresh = false, refreshInterval = 30000, fallbackLanguage = 'en', user, ormConfig = {}, } = options;
const { client } = useGftdOrm(ormConfig);
// @todo [P2] Replace with actual Weblate SDK when available
// Assigned: Frontend Dev | Milestone: Q1 2025 | Impact: Hook functionality
const weblateT = useCallback((key, params) => key, []);
const weblateLanguage = initialLanguage || 'en';
const setWeblateLanguage = useCallback((lang) => {
// @todo [P2] Implement when Weblate SDK is available
// Assigned: Frontend Dev | Milestone: Q1 2025 | Impact: Language switching
log.info('Setting language to:', lang);
}, []);
const [translatorClient, setTranslatorClient] = useState(null);
const [currentLanguage, setCurrentLanguage] = useState(initialLanguage || weblateLanguage || 'en');
const [translations, setTranslations] = useState([]);
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [initialized, setInitialized] = useState(false);
// 翻訳クライアントを初期化
useEffect(() => {
if (!initialized) {
try {
const config = translatorConfig.fromEnv();
const client = TranslatorClient.getInstance(config);
if (user) {
client.setCurrentUser(user);
}
setTranslatorClient(client);
setInitialized(true);
log.info('Translator client initialized in hook');
}
catch (error) {
log.error(`Failed to initialize translator client: ${error}`);
setError(String(error));
}
}
}, [initialized, user]);
// 言語変更時にWeblateの言語も更新
useEffect(() => {
if (currentLanguage !== weblateLanguage) {
setWeblateLanguage(currentLanguage);
}
}, [currentLanguage, weblateLanguage, setWeblateLanguage]);
// 翻訳データを取得
const fetchTranslations = useCallback(async (lang) => {
if (!translatorClient)
return;
const targetLanguage = lang || currentLanguage;
setLoading(true);
setError(null);
try {
const response = await translatorClient.getTranslations(targetLanguage, {
limit: 1000,
});
setTranslations(response.results);
log.info(`Fetched ${response.results.length} translations for language: ${targetLanguage}`);
}
catch (error) {
log.error(`Failed to fetch translations: ${error}`);
setError(String(error));
}
finally {
setLoading(false);
}
}, [translatorClient, currentLanguage]);
// 統計情報を取得
const fetchStats = useCallback(async (lang) => {
if (!translatorClient)
return;
const targetLanguage = lang || currentLanguage;
try {
const stats = await translatorClient.getTranslationStats(targetLanguage);
setStats(stats);
log.info(`Fetched translation stats for language: ${targetLanguage}`);
}
catch (error) {
log.error(`Failed to fetch translation stats: ${error}`);
setError(String(error));
}
}, [translatorClient, currentLanguage]);
// 初期データロード
useEffect(() => {
if (translatorClient && initialized) {
fetchTranslations();
fetchStats();
}
}, [translatorClient, initialized, fetchTranslations, fetchStats]);
// 自動更新
useEffect(() => {
if (!autoRefresh || !translatorClient)
return;
const interval = setInterval(() => {
fetchTranslations();
fetchStats();
}, refreshInterval);
return () => clearInterval(interval);
}, [autoRefresh, translatorClient, refreshInterval, fetchTranslations, fetchStats]);
// 翻訳関数
const t = useCallback((key, params, options) => {
const { namespace: ns = namespace, fallback, interpolation = true, } = options || {};
// まずWeblateの翻訳を試す
try {
const translation = weblateT(key, params);
if (translation && translation !== key) {
return translation;
}
}
catch (error) {
log.warn(`Weblate translation failed for key: ${key}`);
}
// ローカルの翻訳データから検索
const localTranslation = translations.find(item => item.key === key || item.key === `${ns}.${key}`);
if (localTranslation && localTranslation.translated && localTranslation.text) {
let result = localTranslation.text;
// パラメータ補間
if (interpolation && params) {
Object.keys(params).forEach(param => {
const value = params[param];
result = result.replace(new RegExp(`{{${param}}}`, 'g'), String(value));
});
}
return result;
}
// フォールバック
if (fallback) {
return fallback;
}
// デフォルトフォールバック
return key;
}, [weblateT, translations, namespace]);
// 翻訳項目を更新
const updateTranslation = useCallback(async (key, value, options) => {
if (!translatorClient) {
throw new Error('Translator client not initialized');
}
const targetLanguage = options?.language || currentLanguage;
const translation = translations.find(item => item.key === key || item.key.endsWith(`.${key}`));
if (!translation) {
throw new Error(`Translation not found for key: ${key}`);
}
try {
const updated = await translatorClient.updateTranslation(translation.id, value, targetLanguage, {
state: options?.state,
});
// ローカルの翻訳データを更新
setTranslations(prev => prev.map(item => item.id === translation.id ? { ...item, ...updated } : item));
log.info(`Translation updated: ${key} = ${value}`);
return updated;
}
catch (error) {
log.error(`Failed to update translation: ${error}`);
throw error;
}
}, [translatorClient, currentLanguage, translations]);
// 翻訳を検索
const searchTranslations = useCallback(async (query, options) => {
if (!translatorClient) {
throw new Error('Translator client not initialized');
}
const targetLanguage = options?.language || currentLanguage;
try {
const result = await translatorClient.searchTranslations(query, {
language: targetLanguage,
limit: options?.limit,
});
return result;
}
catch (error) {
log.error(`Failed to search translations: ${error}`);
throw error;
}
}, [translatorClient, currentLanguage]);
// 言語を変更
const changeLanguage = useCallback(async (lang) => {
setCurrentLanguage(lang);
setWeblateLanguage(lang);
if (translatorClient) {
await fetchTranslations(lang);
await fetchStats(lang);
}
}, [translatorClient, fetchTranslations, fetchStats, setWeblateLanguage]);
// 利用可能な言語を取得
const supportedLanguages = useMemo(() => {
if (!translatorClient)
return [];
// 設定から取得
const config = translatorConfig.fromEnv();
return config.supportedLanguages;
}, [translatorClient]);
// 翻訳の進捗率を計算
const progress = useMemo(() => {
if (!stats)
return 0;
return stats.translated_percent || 0;
}, [stats]);
// 未翻訳の項目数を取得
const untranslatedCount = useMemo(() => {
if (!stats)
return 0;
return stats.total - stats.translated;
}, [stats]);
// 翻訳が必要な項目をフィルタ
const pendingTranslations = useMemo(() => {
return translations.filter(item => !item.translated || item.fuzzy || item.state === 'needs-editing');
}, [translations]);
// 権限チェック
const canEdit = useMemo(() => {
if (!translatorClient || !user)
return false;
try {
return translatorClient.checkTranslationPermission('write');
}
catch (error) {
return false;
}
}, [translatorClient, user]);
const canManage = useMemo(() => {
if (!translatorClient || !user)
return false;
try {
return translatorClient.checkTranslationPermission('manage');
}
catch (error) {
return false;
}
}, [translatorClient, user]);
// デバッグ情報
const debugInfo = useMemo(() => ({
initialized,
translatorClient: !!translatorClient,
currentLanguage,
translationsCount: translations.length,
stats,
supportedLanguages,
canEdit,
canManage,
loading,
error,
}), [
initialized,
translatorClient,
currentLanguage,
translations.length,
stats,
supportedLanguages,
canEdit,
canManage,
loading,
error,
]);
return {
// 翻訳機能
t,
updateTranslation,
searchTranslations,
// 言語管理
currentLanguage,
changeLanguage,
supportedLanguages,
// データ
translations,
pendingTranslations,
stats,
progress,
untranslatedCount,
// 状態
loading,
error,
initialized,
// 権限
canEdit,
canManage,
// 操作
refresh: fetchTranslations,
refreshStats: fetchStats,
// クライアント
client: translatorClient,
// デバッグ
debugInfo,
};
}
/**
* 翻訳管理用フック(管理者向け)
*/
export function useTranslatorAdmin() {
const { client: translatorClient, canManage } = useTranslator();
const [projects, setProjects] = useState([]);
const [components, setComponents] = useState([]);
const [languages, setLanguages] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// プロジェクト一覧を取得
const fetchProjects = useCallback(async () => {
if (!translatorClient || !canManage)
return;
setLoading(true);
setError(null);
try {
const projectList = await translatorClient.getProjects();
setProjects(projectList);
}
catch (error) {
log.error(`Failed to fetch projects: ${error}`);
setError(String(error));
}
finally {
setLoading(false);
}
}, [translatorClient, canManage]);
// コンポーネント一覧を取得
const fetchComponents = useCallback(async (projectSlug) => {
if (!translatorClient || !canManage)
return;
setLoading(true);
setError(null);
try {
const componentList = await translatorClient.getComponents(projectSlug);
setComponents(componentList);
}
catch (error) {
log.error(`Failed to fetch components: ${error}`);
setError(String(error));
}
finally {
setLoading(false);
}
}, [translatorClient, canManage]);
// 言語一覧を取得
const fetchLanguages = useCallback(async (projectSlug) => {
if (!translatorClient || !canManage)
return;
setLoading(true);
setError(null);
try {
const languageList = await translatorClient.getLanguages(projectSlug);
setLanguages(languageList);
}
catch (error) {
log.error(`Failed to fetch languages: ${error}`);
setError(String(error));
}
finally {
setLoading(false);
}
}, [translatorClient, canManage]);
// プロジェクト作成
const createProject = useCallback(async (project) => {
if (!translatorClient || !canManage) {
throw new Error('Permission denied');
}
try {
const created = await translatorClient.createProject(project);
await fetchProjects(); // リフレッシュ
return created;
}
catch (error) {
log.error(`Failed to create project: ${error}`);
throw error;
}
}, [translatorClient, canManage, fetchProjects]);
// コンポーネント作成
const createComponent = useCallback(async (projectSlug, component) => {
if (!translatorClient || !canManage) {
throw new Error('Permission denied');
}
try {
const created = await translatorClient.createComponent(projectSlug, component);
await fetchComponents(projectSlug); // リフレッシュ
return created;
}
catch (error) {
log.error(`Failed to create component: ${error}`);
throw error;
}
}, [translatorClient, canManage, fetchComponents]);
// テナント用プロジェクト初期化
const initializeTenantProject = useCallback(async (tenantId) => {
if (!translatorClient || !canManage) {
throw new Error('Permission denied');
}
try {
const project = await translatorClient.initializeTenantProject(tenantId);
await fetchProjects(); // リフレッシュ
return project;
}
catch (error) {
log.error(`Failed to initialize tenant project: ${error}`);
throw error;
}
}, [translatorClient, canManage, fetchProjects]);
useEffect(() => {
if (translatorClient && canManage) {
fetchProjects();
}
}, [translatorClient, canManage, fetchProjects]);
return {
// データ
projects,
components,
languages,
// 状態
loading,
error,
canManage,
// 操作
fetchProjects,
fetchComponents,
fetchLanguages,
createProject,
createComponent,
initializeTenantProject,
// クライアント
client: translatorClient,
};
}
/**
* 翻訳統計用フック
*/
export function useTranslationStats(language) {
const { client: translatorClient, currentLanguage } = useTranslator();
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const targetLanguage = language || currentLanguage;
const fetchStats = useCallback(async () => {
if (!translatorClient)
return;
setLoading(true);
setError(null);
try {
const translationStats = await translatorClient.getTranslationStats(targetLanguage);
setStats(translationStats);
}
catch (error) {
log.error(`Failed to fetch translation stats: ${error}`);
setError(String(error));
}
finally {
setLoading(false);
}
}, [translatorClient, targetLanguage]);
useEffect(() => {
if (translatorClient) {
fetchStats();
}
}, [translatorClient, fetchStats]);
const progressInfo = useMemo(() => {
if (!stats)
return null;
return {
total: stats.total,
translated: stats.translated,
approved: stats.approved,
fuzzy: stats.fuzzy,
pending: stats.pending,
translatedPercent: stats.translated_percent,
approvedPercent: stats.approved_percent,
fuzzyPercent: stats.fuzzy_percent,
pendingPercent: stats.pending_percent,
wordsTotal: stats.total_words,
wordsTranslated: stats.translated_words,
wordsApproved: stats.approved_words,
wordsTranslatedPercent: stats.translated_words_percent,
wordsApprovedPercent: stats.approved_words_percent,
lastChange: stats.last_change,
lastAuthor: stats.last_author,
};
}, [stats]);
return {
stats,
progressInfo,
loading,
error,
refresh: fetchStats,
language: targetLanguage,
};
}
//# sourceMappingURL=useTranslator.js.map