musubi-sdd 3.6.1 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,288 @@
1
+ /**
2
+ * @fileoverview Multi-language Template Manager
3
+ * @module templates/locale-manager
4
+ * @description Manages localized templates for MUSUBI SDD
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs-extra');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Supported locales
14
+ */
15
+ const SUPPORTED_LOCALES = ['en', 'ja', 'zh', 'ko', 'es', 'de', 'fr'];
16
+
17
+ /**
18
+ * Template category to directory mapping
19
+ */
20
+ const TEMPLATE_CATEGORIES = {
21
+ requirements: 'requirements',
22
+ design: 'design',
23
+ tasks: 'tasks',
24
+ adr: 'adr-template',
25
+ research: 'research',
26
+ 'workflow-guide': 'workflow-guide',
27
+ };
28
+
29
+ /**
30
+ * Locale display names
31
+ */
32
+ const LOCALE_NAMES = {
33
+ en: 'English',
34
+ ja: '日本語',
35
+ zh: '中文',
36
+ ko: '한국어',
37
+ es: 'Español',
38
+ de: 'Deutsch',
39
+ fr: 'Français',
40
+ };
41
+
42
+ /**
43
+ * Multi-language Template Manager
44
+ */
45
+ class LocaleManager {
46
+ /**
47
+ * Create a LocaleManager instance
48
+ * @param {string} projectPath - Project root path
49
+ * @param {Object} options - Configuration options
50
+ */
51
+ constructor(projectPath, options = {}) {
52
+ this.projectPath = projectPath;
53
+ this.templatesPath = path.join(projectPath, 'steering', 'templates');
54
+ this.defaultLocale = options.defaultLocale || 'en';
55
+ this.fallbackLocale = options.fallbackLocale || 'en';
56
+ }
57
+
58
+ /**
59
+ * Get supported locales
60
+ * @returns {string[]} Locale codes
61
+ */
62
+ getSupportedLocales() {
63
+ return [...SUPPORTED_LOCALES];
64
+ }
65
+
66
+ /**
67
+ * Get locale display name
68
+ * @param {string} locale - Locale code
69
+ * @returns {string} Display name
70
+ */
71
+ getLocaleName(locale) {
72
+ return LOCALE_NAMES[locale] || locale;
73
+ }
74
+
75
+ /**
76
+ * Get template for specific locale
77
+ * @param {string} category - Template category
78
+ * @param {string} locale - Locale code
79
+ * @returns {Promise<string|null>} Template content or null
80
+ */
81
+ async getTemplate(category, locale = this.defaultLocale) {
82
+ const baseName = TEMPLATE_CATEGORIES[category] || category;
83
+
84
+ // Try exact locale match first
85
+ let templatePath = this.getTemplatePath(baseName, locale);
86
+ if (await fs.pathExists(templatePath)) {
87
+ return fs.readFile(templatePath, 'utf-8');
88
+ }
89
+
90
+ // Try fallback locale
91
+ if (locale !== this.fallbackLocale) {
92
+ templatePath = this.getTemplatePath(baseName, this.fallbackLocale);
93
+ if (await fs.pathExists(templatePath)) {
94
+ return fs.readFile(templatePath, 'utf-8');
95
+ }
96
+ }
97
+
98
+ // Try base template (no locale suffix)
99
+ templatePath = path.join(this.templatesPath, `${baseName}.md`);
100
+ if (await fs.pathExists(templatePath)) {
101
+ return fs.readFile(templatePath, 'utf-8');
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ /**
108
+ * Get template path for locale
109
+ * @param {string} baseName - Template base name
110
+ * @param {string} locale - Locale code
111
+ * @returns {string} Template file path
112
+ */
113
+ getTemplatePath(baseName, locale) {
114
+ if (locale === 'en') {
115
+ return path.join(this.templatesPath, `${baseName}.md`);
116
+ }
117
+ return path.join(this.templatesPath, `${baseName}.${locale}.md`);
118
+ }
119
+
120
+ /**
121
+ * List available templates
122
+ * @returns {Promise<Object>} Map of category -> available locales
123
+ */
124
+ async listTemplates() {
125
+ const result = {};
126
+
127
+ if (!await fs.pathExists(this.templatesPath)) {
128
+ return result;
129
+ }
130
+
131
+ const files = await fs.readdir(this.templatesPath);
132
+
133
+ for (const file of files) {
134
+ if (!file.endsWith('.md')) continue;
135
+
136
+ const { category, locale } = this.parseTemplateFilename(file);
137
+ if (!category) continue;
138
+
139
+ if (!result[category]) {
140
+ result[category] = [];
141
+ }
142
+ if (!result[category].includes(locale)) {
143
+ result[category].push(locale);
144
+ }
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ /**
151
+ * Parse template filename to extract category and locale
152
+ * @param {string} filename - Template filename
153
+ * @returns {{category: string, locale: string}} Parsed info
154
+ */
155
+ parseTemplateFilename(filename) {
156
+ const baseName = filename.replace('.md', '');
157
+
158
+ // Check for locale suffix
159
+ for (const locale of SUPPORTED_LOCALES) {
160
+ if (baseName.endsWith(`.${locale}`)) {
161
+ return {
162
+ category: baseName.slice(0, -(locale.length + 1)),
163
+ locale,
164
+ };
165
+ }
166
+ }
167
+
168
+ // No locale suffix means English
169
+ return {
170
+ category: baseName,
171
+ locale: 'en',
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Create template for new locale
177
+ * @param {string} category - Template category
178
+ * @param {string} locale - Target locale
179
+ * @param {string} content - Translated content
180
+ * @returns {Promise<string>} Created file path
181
+ */
182
+ async createLocalizedTemplate(category, locale, content) {
183
+ const baseName = TEMPLATE_CATEGORIES[category] || category;
184
+ const templatePath = this.getTemplatePath(baseName, locale);
185
+
186
+ await fs.ensureDir(path.dirname(templatePath));
187
+ await fs.writeFile(templatePath, content, 'utf-8');
188
+
189
+ return templatePath;
190
+ }
191
+
192
+ /**
193
+ * Get translation suggestions for a template
194
+ * @param {string} content - Template content
195
+ * @param {string} targetLocale - Target locale
196
+ * @returns {Object} Translation metadata
197
+ */
198
+ getTranslationMetadata(content, targetLocale) {
199
+ // Extract translatable sections
200
+ const sections = [];
201
+ const lines = content.split('\n');
202
+
203
+ let inCodeBlock = false;
204
+
205
+ for (let i = 0; i < lines.length; i++) {
206
+ const line = lines[i];
207
+
208
+ if (line.startsWith('```')) {
209
+ inCodeBlock = !inCodeBlock;
210
+ continue;
211
+ }
212
+
213
+ if (inCodeBlock) continue;
214
+
215
+ // Headers
216
+ if (line.startsWith('#')) {
217
+ sections.push({
218
+ type: 'header',
219
+ line: i + 1,
220
+ content: line,
221
+ translate: true,
222
+ });
223
+ }
224
+
225
+ // Paragraphs (non-empty, non-special lines)
226
+ if (line.trim() && !line.startsWith('-') && !line.startsWith('*') && !line.startsWith('|')) {
227
+ sections.push({
228
+ type: 'text',
229
+ line: i + 1,
230
+ content: line,
231
+ translate: true,
232
+ });
233
+ }
234
+ }
235
+
236
+ return {
237
+ locale: targetLocale,
238
+ localeName: LOCALE_NAMES[targetLocale] || targetLocale,
239
+ totalLines: lines.length,
240
+ translatableSections: sections.length,
241
+ sections,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Detect locale from project configuration
247
+ * @returns {Promise<string>} Detected locale
248
+ */
249
+ async detectProjectLocale() {
250
+ // Check project.yml
251
+ const projectPath = path.join(this.projectPath, 'steering', 'project.yml');
252
+ if (await fs.pathExists(projectPath)) {
253
+ const yaml = require('js-yaml');
254
+ const content = await fs.readFile(projectPath, 'utf-8');
255
+ const config = yaml.load(content);
256
+ if (config?.locale) {
257
+ return config.locale;
258
+ }
259
+ }
260
+
261
+ // Check steering files for locale hints
262
+ const structurePath = path.join(this.projectPath, 'steering', 'structure.md');
263
+ if (await fs.pathExists(structurePath)) {
264
+ const content = await fs.readFile(structurePath, 'utf-8');
265
+ // Check for Japanese characters
266
+ if (/[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]/.test(content)) {
267
+ return 'ja';
268
+ }
269
+ // Check for Chinese characters (without Japanese hiragana/katakana)
270
+ if (/[\u4E00-\u9FFF]/.test(content) && !/[\u3040-\u309F\u30A0-\u30FF]/.test(content)) {
271
+ return 'zh';
272
+ }
273
+ // Check for Korean
274
+ if (/[\uAC00-\uD7AF]/.test(content)) {
275
+ return 'ko';
276
+ }
277
+ }
278
+
279
+ return this.defaultLocale;
280
+ }
281
+ }
282
+
283
+ module.exports = {
284
+ LocaleManager,
285
+ SUPPORTED_LOCALES,
286
+ LOCALE_NAMES,
287
+ TEMPLATE_CATEGORIES,
288
+ };