eslint-plugin-smarthr 6.20.0 → 6.21.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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -0
- package/package.json +2 -2
- package/rules/best-practice-for-unstable-dependencies/README.md +52 -0
- package/rules/require-i18n-translation-sync/README.md +365 -0
- package/rules/require-i18n-translation-sync/index.js +277 -0
- package/test/require-i18n-translation-sync.js +309 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [6.21.1](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.21.0...eslint-plugin-smarthr-v6.21.1) (2026-06-22)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **eslint-plugin-smarthr:** use https repository metadata ([#1407](https://github.com/kufu/tamatebako/issues/1407)) ([e6d3d82](https://github.com/kufu/tamatebako/commit/e6d3d82d4d833a2716c67d91f82fe7bd6e2ab6cc))
|
|
11
|
+
|
|
12
|
+
## [6.21.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.20.0...eslint-plugin-smarthr-v6.21.0) (2026-06-22)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* require-i18n-translation-sync ESLintルールを追加 ([#1406](https://github.com/kufu/tamatebako/issues/1406)) ([5286bbb](https://github.com/kufu/tamatebako/commit/5286bbb94c6b774f9132f6d40ef36c216cff0399))
|
|
18
|
+
|
|
5
19
|
## [6.20.0](https://github.com/kufu/tamatebako/compare/eslint-plugin-smarthr-v6.19.0...eslint-plugin-smarthr-v6.20.0) (2026-06-16)
|
|
6
20
|
|
|
7
21
|
|
package/README.md
CHANGED
|
@@ -57,5 +57,6 @@
|
|
|
57
57
|
- [require-declaration](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-declaration)
|
|
58
58
|
- [require-export](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-export)
|
|
59
59
|
- [require-i18n-text](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-text)
|
|
60
|
+
- [require-i18n-translation-sync](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-i18n-translation-sync)
|
|
60
61
|
- [require-import](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/require-import)
|
|
61
62
|
- [trim-props](https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr/rules/trim-props)
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-smarthr",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.21.1",
|
|
4
4
|
"author": "SmartHR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A sharable ESLint plugin for SmartHR",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+
|
|
10
|
+
"url": "git+https://github.com/kufu/tamatebako.git"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://github.com/kufu/tamatebako/tree/master/packages/eslint-plugin-smarthr",
|
|
13
13
|
"publishConfig": {
|
|
@@ -205,6 +205,58 @@ useEffect(() => {
|
|
|
205
205
|
|
|
206
206
|
**注意:** メモ化は依存関係が明確な場合に有効です。依存関係が不明確な場合は、方法1のrefを使用することを推奨します。
|
|
207
207
|
|
|
208
|
+
### 方法5: イベントオブジェクトから値を取得する(useCallbackの場合)
|
|
209
|
+
|
|
210
|
+
useCallbackでイベントハンドラーを定義する場合、値を依存配列に含めずに、イベントオブジェクトから取得できます。
|
|
211
|
+
|
|
212
|
+
**button要素の`value`属性を使う:**
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
// ❌ 値を依存配列に含める
|
|
216
|
+
const handleClick = useCallback(() => {
|
|
217
|
+
doSomething(itemId)
|
|
218
|
+
}, [itemId]) // itemIdが変わるたびにコールバックが再生成される
|
|
219
|
+
|
|
220
|
+
<button onClick={handleClick}>削除</button>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
// ✅ イベントから値を取得
|
|
225
|
+
const handleClick = useCallback((e) => {
|
|
226
|
+
const itemId = e.currentTarget.value
|
|
227
|
+
doSomething(itemId)
|
|
228
|
+
}, []) // 依存配列が空になり、コールバックが安定する
|
|
229
|
+
|
|
230
|
+
<button value={itemId} onClick={handleClick}>削除</button>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**data-*属性も同様に使える:**
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
const handleClick = useCallback((e) => {
|
|
237
|
+
const itemId = e.currentTarget.getAttribute('data-id')
|
|
238
|
+
const itemType = e.currentTarget.getAttribute('data-type')
|
|
239
|
+
doSomething(itemId, itemType)
|
|
240
|
+
}, [])
|
|
241
|
+
|
|
242
|
+
<button
|
|
243
|
+
data-id={itemId}
|
|
244
|
+
data-type={itemType}
|
|
245
|
+
onClick={handleClick}
|
|
246
|
+
>
|
|
247
|
+
削除
|
|
248
|
+
</button>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**メリット:**
|
|
252
|
+
- 依存配列が空になり、コールバックが安定する
|
|
253
|
+
- コンポーネントの再レンダリングが減る
|
|
254
|
+
- リスト内の各アイテムで異なるコールバック関数を生成する必要がない
|
|
255
|
+
|
|
256
|
+
**注意:**
|
|
257
|
+
- この方法はイベントハンドラー(onClick、onChange等)でのみ使用できます。useEffect内など、イベントオブジェクトがない場合は他の方法を使用してください。
|
|
258
|
+
- `value`属性や`getAttribute()`で取得した値は**文字列**になります。数値や真偽値が必要な場合は適切に変換してください(例: `Number(e.currentTarget.value)`、`e.currentTarget.value === 'true'`)。
|
|
259
|
+
|
|
208
260
|
## オプション
|
|
209
261
|
|
|
210
262
|
### additionalUnstableNames
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# require-i18n-translation-sync
|
|
2
|
+
|
|
3
|
+
TypeScript翻訳ファイル(ja.ts)とJSON翻訳ファイル(ja.json)の同期を保証するルールです。
|
|
4
|
+
|
|
5
|
+
## 背景
|
|
6
|
+
|
|
7
|
+
多言語翻訳の仕組みでは以下の要件があります:
|
|
8
|
+
|
|
9
|
+
1. **TypeScript形式が必要な理由**
|
|
10
|
+
- 翻訳キーの型定義により、存在しないキーの使用を静的解析でエラーに
|
|
11
|
+
- FormattedMessageの`defaultMessage`が翻訳ファイルの値と一致することを型で保証
|
|
12
|
+
|
|
13
|
+
2. **JSON形式が必要な理由**
|
|
14
|
+
- 全言語でキー構造を揃える必要がある(翻訳ツールの仕様)
|
|
15
|
+
- 翻訳ツールとの互換性
|
|
16
|
+
|
|
17
|
+
3. **SSOT(Single Source of Truth)の原則**
|
|
18
|
+
- ja.ts と ja.json を別々に定義すると内容の食い違いが発生
|
|
19
|
+
- ja.ts をソースとして、ja.json を自動生成する
|
|
20
|
+
|
|
21
|
+
このルールは、ja.ts と ja.json の内容が一致することを保証し、不一致があれば自動修正します。
|
|
22
|
+
|
|
23
|
+
## チェック内容
|
|
24
|
+
|
|
25
|
+
### 1. オブジェクトリテラルのexportは1つのみ
|
|
26
|
+
|
|
27
|
+
翻訳ファイルは必ず1つのオブジェクトリテラルをexportする必要があります。型定義など、オブジェクトリテラル以外のexportは複数あっても問題ありません。
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// ❌ NG: オブジェクトリテラルが複数
|
|
31
|
+
export const translations = { ... }
|
|
32
|
+
export const other = { ... }
|
|
33
|
+
|
|
34
|
+
// ✅ OK: オブジェクトリテラルは1つのみ
|
|
35
|
+
export const translations = { ... }
|
|
36
|
+
|
|
37
|
+
// ✅ OK: 型定義とオブジェクトリテラル
|
|
38
|
+
export type TranslationKeys = keyof typeof translations
|
|
39
|
+
export const translations = { ... }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. フラットな構造
|
|
43
|
+
|
|
44
|
+
翻訳ファイルはフラットなオブジェクト構造である必要があります。
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// ❌ NG: ネストされたオブジェクト
|
|
48
|
+
export const translations = {
|
|
49
|
+
Common: {
|
|
50
|
+
Button: {
|
|
51
|
+
Submit: '送信'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ✅ OK: フラットな構造
|
|
57
|
+
export const translations = {
|
|
58
|
+
'Common/Button/Submit': '送信'
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. 値は文字列のみ
|
|
63
|
+
|
|
64
|
+
オブジェクトの値はすべて文字列である必要があります。
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// ❌ NG: 数値、真偽値
|
|
68
|
+
export const translations = {
|
|
69
|
+
count: 123,
|
|
70
|
+
enabled: true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ✅ OK: 文字列のみ
|
|
74
|
+
export const translations = {
|
|
75
|
+
count: '123',
|
|
76
|
+
enabled: 'true'
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4. JSONとの同期
|
|
81
|
+
|
|
82
|
+
ja.ts と ja.json の内容が一致する必要があります。不一致の場合、autofixで ja.json を自動生成します。
|
|
83
|
+
|
|
84
|
+
## エラーケース
|
|
85
|
+
|
|
86
|
+
### オブジェクトリテラルが複数
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ❌ NG
|
|
90
|
+
export const translations = { key1: 'value1' }
|
|
91
|
+
export const other = { key2: 'value2' }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**エラー:** 翻訳ファイルはオブジェクトリテラルを1つのみexportする必要があります。
|
|
95
|
+
|
|
96
|
+
### オブジェクトリテラルのexportがない
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// ❌ NG: exportがない
|
|
100
|
+
const translations = { key1: 'value1' }
|
|
101
|
+
|
|
102
|
+
// ❌ NG: オブジェクトリテラル以外をexport
|
|
103
|
+
export const translations = 'not an object'
|
|
104
|
+
export const translations = ['array']
|
|
105
|
+
|
|
106
|
+
// ❌ NG: 型のみをexport
|
|
107
|
+
export type TranslationKeys = string
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**エラー:** 翻訳ファイルはオブジェクトリテラルをexportする必要があります。
|
|
111
|
+
|
|
112
|
+
### 値が文字列以外
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// ❌ NG
|
|
116
|
+
export const translations = {
|
|
117
|
+
count: 123,
|
|
118
|
+
enabled: true,
|
|
119
|
+
value: null
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**エラー:** キー "count" の値は文字列である必要があります。
|
|
124
|
+
|
|
125
|
+
### ネストされたオブジェクト
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ❌ NG
|
|
129
|
+
export const translations = {
|
|
130
|
+
Common: {
|
|
131
|
+
Button: 'ボタン'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**エラー:** キー "Common" の値はネストされたオブジェクトです。翻訳ファイルはフラットな構造である必要があります。
|
|
137
|
+
|
|
138
|
+
### スプレッド構文
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// ❌ NG
|
|
142
|
+
const base = { key1: 'value1' }
|
|
143
|
+
export const translations = { ...base, key2: 'value2' }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**エラー:** スプレッド構文は使用できません。
|
|
147
|
+
|
|
148
|
+
### テンプレートリテラルに式を含む
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// ❌ NG
|
|
152
|
+
export const translations = {
|
|
153
|
+
message: `Hello ${name}`
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**エラー:** テンプレートリテラルに式を含めることはできません。
|
|
158
|
+
|
|
159
|
+
### ja.ts と ja.json が不一致
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// ja.ts
|
|
163
|
+
export const translations = { key1: 'new value' }
|
|
164
|
+
|
|
165
|
+
// ja.json(古い内容)
|
|
166
|
+
{
|
|
167
|
+
"key1": "old value"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**エラー:** 翻訳ファイル(ja.ts)とJSONファイル(ja.json)の内容が一致しません。autofixで同期できます。
|
|
172
|
+
|
|
173
|
+
**修正:** ESLintの`--fix`オプションを使用すると、ja.json が自動的に更新されます。
|
|
174
|
+
|
|
175
|
+
## OK例
|
|
176
|
+
|
|
177
|
+
### export const
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
export const translations = {
|
|
181
|
+
'Common/Button/Submit': '送信',
|
|
182
|
+
'Common/Button/Cancel': 'キャンセル'
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### export default
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
export default {
|
|
190
|
+
'Common/Button/Submit': '送信',
|
|
191
|
+
'Common/Button/Cancel': 'キャンセル'
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 型アサーション
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
export const translations = {
|
|
199
|
+
'Common/Button/Submit': '送信'
|
|
200
|
+
} as const
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 型注釈
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
export const translations: Record<string, string> = {
|
|
207
|
+
'Common/Button/Submit': '送信'
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### テンプレートリテラル(式なし)
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
export const translations = {
|
|
215
|
+
message: `Hello`
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Identifierキー
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
export const translations = {
|
|
223
|
+
submit: '送信',
|
|
224
|
+
cancel: 'キャンセル'
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 型定義との併用
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
export type TranslationKeys = keyof typeof translations
|
|
232
|
+
|
|
233
|
+
export const translations = {
|
|
234
|
+
'Common/Button/Submit': '送信',
|
|
235
|
+
'Common/Button/Cancel': 'キャンセル'
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## オプション
|
|
240
|
+
|
|
241
|
+
### targetFileName
|
|
242
|
+
|
|
243
|
+
対象とするファイル名を指定します。デフォルトは`ja.ts`です。
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
{
|
|
247
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
248
|
+
targetFileName: 'en_us.ts'
|
|
249
|
+
}]
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### indent
|
|
254
|
+
|
|
255
|
+
JSON出力時のインデント幅を指定します。デフォルトは`2`(2スペース)です。
|
|
256
|
+
|
|
257
|
+
```javascript
|
|
258
|
+
{
|
|
259
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
260
|
+
indent: 4
|
|
261
|
+
}]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### endOfLine
|
|
266
|
+
|
|
267
|
+
JSON出力時の改行コードを指定します。デフォルトは`lf`です。
|
|
268
|
+
|
|
269
|
+
- `lf`: LF(`\n`)Unix/Linux/macOS標準
|
|
270
|
+
- `crlf`: CRLF(`\r\n`)Windows標準
|
|
271
|
+
|
|
272
|
+
```javascript
|
|
273
|
+
{
|
|
274
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
275
|
+
endOfLine: 'crlf'
|
|
276
|
+
}]
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## 使用例
|
|
281
|
+
|
|
282
|
+
### 推奨: 対象ファイルのみに適用(効率的)
|
|
283
|
+
|
|
284
|
+
**このルールは対象ファイル(デフォルト: ja.ts)のみをチェックするため、全ファイルに対して実行すると無駄なチェックが発生します。**
|
|
285
|
+
|
|
286
|
+
ESLintの `files` オプションで対象ファイルを指定することで、効率的にチェックできます。
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
export default [
|
|
290
|
+
{
|
|
291
|
+
files: ['**/ja.ts'],
|
|
292
|
+
rules: {
|
|
293
|
+
'smarthr/require-i18n-translation-sync': 'error'
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 基本的な使用(非推奨)
|
|
300
|
+
|
|
301
|
+
全ファイルに対してルールを有効化する場合(対象外ファイルは内部でスキップされますが効率が悪いです):
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
{
|
|
305
|
+
'smarthr/require-i18n-translation-sync': 'error'
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### カスタム設定
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
export default [
|
|
313
|
+
{
|
|
314
|
+
files: ['**/ja.ts'],
|
|
315
|
+
rules: {
|
|
316
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
317
|
+
targetFileName: 'ja.ts',
|
|
318
|
+
indent: 2,
|
|
319
|
+
endOfLine: 'lf'
|
|
320
|
+
}]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### 複数の言語ファイルに対応
|
|
327
|
+
|
|
328
|
+
英語の翻訳ファイルも同様にチェックする場合:
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
export default [
|
|
332
|
+
{
|
|
333
|
+
files: ['**/ja.ts'],
|
|
334
|
+
rules: {
|
|
335
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
336
|
+
targetFileName: 'ja.ts'
|
|
337
|
+
}]
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
files: ['**/en.ts'],
|
|
342
|
+
rules: {
|
|
343
|
+
'smarthr/require-i18n-translation-sync': ['error', {
|
|
344
|
+
targetFileName: 'en.ts'
|
|
345
|
+
}]
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## 自動修正
|
|
352
|
+
|
|
353
|
+
このルールは`--fix`オプションで自動修正できます。
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
eslint --fix path/to/ja.ts
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
自動修正により、ja.ts から抽出した翻訳データが ja.json に書き込まれます。
|
|
360
|
+
|
|
361
|
+
## 注意事項
|
|
362
|
+
|
|
363
|
+
- このルールは指定されたファイル名(デフォルト: ja.ts)にのみ適用されます
|
|
364
|
+
- JSONファイルのキー順序は、TypeScriptファイルの記述順を保持します
|
|
365
|
+
- 他言語ファイル(en.json, zh.json など)は翻訳ツールで管理し、開発者が直接編集することは想定していません
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const SCHEMA = [
|
|
5
|
+
{
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
targetFileName: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
default: 'ja.ts',
|
|
11
|
+
},
|
|
12
|
+
indent: {
|
|
13
|
+
type: 'number',
|
|
14
|
+
default: 2,
|
|
15
|
+
},
|
|
16
|
+
endOfLine: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
enum: ['lf', 'crlf'],
|
|
19
|
+
default: 'lf',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* オブジェクトリテラルから翻訳データを抽出
|
|
28
|
+
* @param {import('estree').ObjectExpression} node
|
|
29
|
+
* @param {import('@typescript-eslint/utils').TSESLint.SourceCode} sourceCode
|
|
30
|
+
* @returns {{ translations: Record<string, string>, errors: Array<{ node: any, message: string }> }}
|
|
31
|
+
*/
|
|
32
|
+
function extractTranslations(node, sourceCode) {
|
|
33
|
+
const translations = {}
|
|
34
|
+
const errors = []
|
|
35
|
+
|
|
36
|
+
for (const prop of node.properties) {
|
|
37
|
+
// SpreadElementは不許可
|
|
38
|
+
if (prop.type === 'SpreadElement') {
|
|
39
|
+
errors.push({
|
|
40
|
+
node: prop,
|
|
41
|
+
message: 'スプレッド構文は使用できません。翻訳ファイルはフラットなオブジェクトである必要があります。',
|
|
42
|
+
})
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// キーの取得
|
|
47
|
+
let key
|
|
48
|
+
switch (prop.key.type) {
|
|
49
|
+
case 'Identifier':
|
|
50
|
+
key = prop.key.name
|
|
51
|
+
break
|
|
52
|
+
case 'Literal':
|
|
53
|
+
key = String(prop.key.value)
|
|
54
|
+
break
|
|
55
|
+
default:
|
|
56
|
+
errors.push({
|
|
57
|
+
node: prop.key,
|
|
58
|
+
message: 'キーは文字列リテラルまたは識別子である必要があります。',
|
|
59
|
+
})
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 値の検証
|
|
64
|
+
switch (prop.value.type) {
|
|
65
|
+
case 'Literal':
|
|
66
|
+
if (typeof prop.value.value !== 'string') {
|
|
67
|
+
errors.push({
|
|
68
|
+
node: prop.value,
|
|
69
|
+
message: `キー "${key}" の値は文字列である必要があります。数値、真偽値などは使用できません。`,
|
|
70
|
+
})
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
translations[key] = prop.value.value
|
|
74
|
+
break
|
|
75
|
+
case 'TemplateLiteral':
|
|
76
|
+
// テンプレートリテラルで式がない場合のみ許可
|
|
77
|
+
if (prop.value.expressions.length === 0) {
|
|
78
|
+
translations[key] = prop.value.quasis[0].value.cooked
|
|
79
|
+
} else {
|
|
80
|
+
errors.push({
|
|
81
|
+
node: prop.value,
|
|
82
|
+
message: `キー "${key}" の値は静的な文字列である必要があります。テンプレートリテラルに式を含めることはできません。`,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
case 'ObjectExpression':
|
|
87
|
+
errors.push({
|
|
88
|
+
node: prop.value,
|
|
89
|
+
message: `キー "${key}" の値はネストされたオブジェクトです。翻訳ファイルはフラットな構造である必要があります。`,
|
|
90
|
+
})
|
|
91
|
+
break
|
|
92
|
+
default:
|
|
93
|
+
errors.push({
|
|
94
|
+
node: prop.value,
|
|
95
|
+
message: `キー "${key}" の値は文字列リテラルである必要があります。`,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { translations, errors }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* export文からオブジェクトを探す
|
|
105
|
+
* @param {import('estree').Program} program
|
|
106
|
+
* @returns {{ objectExportCount: number, exportedObject: import('estree').ObjectExpression | null }}
|
|
107
|
+
*/
|
|
108
|
+
function findExportedObject(program) {
|
|
109
|
+
let objectExportCount = 0
|
|
110
|
+
let exportedObject = null
|
|
111
|
+
|
|
112
|
+
for (const node of program.body) {
|
|
113
|
+
// export const translations = { ... }
|
|
114
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration && node.declaration.type === 'VariableDeclaration') {
|
|
115
|
+
for (const decl of node.declaration.declarations) {
|
|
116
|
+
if (decl.init) {
|
|
117
|
+
switch (decl.init.type) {
|
|
118
|
+
case 'ObjectExpression':
|
|
119
|
+
objectExportCount++
|
|
120
|
+
exportedObject = decl.init
|
|
121
|
+
break
|
|
122
|
+
case 'TSAsExpression':
|
|
123
|
+
if (decl.init.expression.type === 'ObjectExpression') {
|
|
124
|
+
// as const などの型アサーション
|
|
125
|
+
objectExportCount++
|
|
126
|
+
exportedObject = decl.init.expression
|
|
127
|
+
}
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// export default { ... }
|
|
135
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
136
|
+
switch (node.declaration.type) {
|
|
137
|
+
case 'ObjectExpression':
|
|
138
|
+
objectExportCount++
|
|
139
|
+
exportedObject = node.declaration
|
|
140
|
+
break
|
|
141
|
+
case 'TSAsExpression':
|
|
142
|
+
if (node.declaration.expression.type === 'ObjectExpression') {
|
|
143
|
+
objectExportCount++
|
|
144
|
+
exportedObject = node.declaration.expression
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { objectExportCount, exportedObject }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @type {import('@typescript-eslint/utils').TSESLint.RuleModule<'multipleExports' | 'noExport' | 'notObject' | 'invalidValue' | 'notSync'>}
|
|
156
|
+
*/
|
|
157
|
+
module.exports = {
|
|
158
|
+
meta: {
|
|
159
|
+
type: 'problem',
|
|
160
|
+
fixable: 'code',
|
|
161
|
+
schema: SCHEMA,
|
|
162
|
+
messages: {
|
|
163
|
+
multipleExports: '翻訳ファイルはオブジェクトリテラルを1つのみexportする必要があります。現在{{count}}個のオブジェクトがexportされています。',
|
|
164
|
+
noExport: '翻訳ファイルはオブジェクトリテラルをexportする必要があります。',
|
|
165
|
+
notObject: 'exportする値はオブジェクトリテラルである必要があります。',
|
|
166
|
+
invalidValue: '{{message}}',
|
|
167
|
+
notSync: '翻訳ファイル({{tsFile}})とJSONファイル({{jsonFile}})の内容が一致しません。autofixで同期できます。',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
create(context) {
|
|
171
|
+
const options = context.options[0] || {}
|
|
172
|
+
const targetFileName = options.targetFileName || 'ja.ts'
|
|
173
|
+
|
|
174
|
+
const filename = context.getFilename()
|
|
175
|
+
const basename = path.basename(filename)
|
|
176
|
+
|
|
177
|
+
// 対象ファイル以外はスキップ
|
|
178
|
+
if (basename !== targetFileName) {
|
|
179
|
+
return {}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
Program(node) {
|
|
184
|
+
const { objectExportCount, exportedObject } = findExportedObject(node)
|
|
185
|
+
|
|
186
|
+
// オブジェクトのexportが複数ある場合
|
|
187
|
+
if (objectExportCount > 1) {
|
|
188
|
+
context.report({
|
|
189
|
+
node,
|
|
190
|
+
messageId: 'multipleExports',
|
|
191
|
+
data: { count: objectExportCount },
|
|
192
|
+
})
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// オブジェクトのexportがない場合
|
|
197
|
+
if (objectExportCount === 0) {
|
|
198
|
+
context.report({
|
|
199
|
+
node,
|
|
200
|
+
messageId: 'noExport',
|
|
201
|
+
})
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// exportがオブジェクトでない場合(念のためのチェック)
|
|
206
|
+
if (!exportedObject) {
|
|
207
|
+
context.report({
|
|
208
|
+
node,
|
|
209
|
+
messageId: 'notObject',
|
|
210
|
+
})
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// オブジェクトから翻訳データを抽出
|
|
215
|
+
const sourceCode = context.getSourceCode()
|
|
216
|
+
const { translations, errors } = extractTranslations(exportedObject, sourceCode)
|
|
217
|
+
|
|
218
|
+
// 構造エラーを報告
|
|
219
|
+
for (const error of errors) {
|
|
220
|
+
context.report({
|
|
221
|
+
node: error.node,
|
|
222
|
+
messageId: 'invalidValue',
|
|
223
|
+
data: { message: error.message },
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// エラーがある場合はJSONとの比較をスキップ
|
|
228
|
+
if (errors.length > 0) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// JSONファイルのパス
|
|
233
|
+
const tsFilePath = filename
|
|
234
|
+
const parsed = path.parse(tsFilePath)
|
|
235
|
+
const jsonFilePath = path.join(parsed.dir, parsed.name + '.json')
|
|
236
|
+
|
|
237
|
+
// JSON文字列を生成
|
|
238
|
+
const indent = options.indent || 2
|
|
239
|
+
const endOfLine = options.endOfLine || 'lf'
|
|
240
|
+
const newline = endOfLine === 'crlf' ? '\r\n' : '\n'
|
|
241
|
+
const jsonString = JSON.stringify(translations, null, indent) + newline
|
|
242
|
+
|
|
243
|
+
// JSONファイルを読み込んで比較
|
|
244
|
+
let existingJsonString = ''
|
|
245
|
+
try {
|
|
246
|
+
if (fs.existsSync(jsonFilePath)) {
|
|
247
|
+
existingJsonString = fs.readFileSync(jsonFilePath, 'utf8')
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
// ファイル読み込みエラーは無視(新規作成として扱う)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 内容が一致しない場合
|
|
254
|
+
if (existingJsonString !== jsonString) {
|
|
255
|
+
const tsFile = path.basename(tsFilePath)
|
|
256
|
+
const jsonFile = path.basename(jsonFilePath)
|
|
257
|
+
|
|
258
|
+
context.report({
|
|
259
|
+
node,
|
|
260
|
+
messageId: 'notSync',
|
|
261
|
+
data: { tsFile, jsonFile },
|
|
262
|
+
fix(fixer) {
|
|
263
|
+
// autofixer: JSONファイルを書き込む
|
|
264
|
+
// ESLintのfixerはファイル書き込みをサポートしないため、
|
|
265
|
+
// ここでは直接ファイルを書き込む
|
|
266
|
+
fs.writeFileSync(jsonFilePath, jsonString, 'utf8')
|
|
267
|
+
// fixerは何も返さない(ファイル外の変更のため)
|
|
268
|
+
return []
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports.schema = SCHEMA
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
|
|
5
|
+
const rule = require('../rules/require-i18n-translation-sync')
|
|
6
|
+
const RuleTester = require('eslint').RuleTester
|
|
7
|
+
|
|
8
|
+
// テスト用の一時ディレクトリ
|
|
9
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-sync-test-'))
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* テスト用のJSONファイルを作成
|
|
13
|
+
*/
|
|
14
|
+
function setupTestFile(basename, translations, options = {}) {
|
|
15
|
+
const tsPath = path.join(tmpDir, basename)
|
|
16
|
+
const parsed = path.parse(basename)
|
|
17
|
+
const jsonPath = path.join(tmpDir, parsed.name + '.json')
|
|
18
|
+
|
|
19
|
+
// JSONファイルを作成
|
|
20
|
+
const indent = options.indent || 2
|
|
21
|
+
const endOfLine = options.endOfLine || 'lf'
|
|
22
|
+
const newline = endOfLine === 'crlf' ? '\r\n' : '\n'
|
|
23
|
+
const jsonString = JSON.stringify(translations, null, indent) + newline
|
|
24
|
+
|
|
25
|
+
fs.writeFileSync(jsonPath, jsonString, 'utf8')
|
|
26
|
+
|
|
27
|
+
return tsPath
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* テスト用のファイルパスを生成(JSONなし)
|
|
32
|
+
*/
|
|
33
|
+
function createTestPath(basename) {
|
|
34
|
+
return path.join(tmpDir, basename)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ruleTester = new RuleTester({
|
|
38
|
+
languageOptions: {
|
|
39
|
+
parser: require('@typescript-eslint/parser'),
|
|
40
|
+
parserOptions: {
|
|
41
|
+
ecmaVersion: 2020,
|
|
42
|
+
sourceType: 'module',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const defaultOptions = [{}]
|
|
48
|
+
|
|
49
|
+
ruleTester.run('require-i18n-translation-sync', rule, {
|
|
50
|
+
valid: [
|
|
51
|
+
// 対象ファイル名でない場合はスキップ
|
|
52
|
+
{
|
|
53
|
+
code: "export const translations = { key1: 'value1' }",
|
|
54
|
+
filename: createTestPath('en.ts'),
|
|
55
|
+
options: defaultOptions,
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// ja.ts と ja.json が一致している場合
|
|
59
|
+
{
|
|
60
|
+
code: "export const translations = { key1: 'value1', key2: 'value2' }",
|
|
61
|
+
filename: setupTestFile('valid-sync.ts', { key1: 'value1', key2: 'value2' }),
|
|
62
|
+
options: defaultOptions,
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// export default でも OK
|
|
66
|
+
{
|
|
67
|
+
code: "export default { key1: 'value1' }",
|
|
68
|
+
filename: setupTestFile('valid-default.ts', { key1: 'value1' }),
|
|
69
|
+
options: defaultOptions,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Identifierキーでも OK
|
|
73
|
+
{
|
|
74
|
+
code: "export const translations = { key1: 'value1', key2: 'value2' }",
|
|
75
|
+
filename: setupTestFile('valid-identifier.ts', { key1: 'value1', key2: 'value2' }),
|
|
76
|
+
options: defaultOptions,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// テンプレートリテラル(式なし)
|
|
80
|
+
{
|
|
81
|
+
code: 'export const translations = { key1: `value1` }',
|
|
82
|
+
filename: setupTestFile('valid-template.ts', { key1: 'value1' }),
|
|
83
|
+
options: defaultOptions,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// カスタムファイル名
|
|
87
|
+
{
|
|
88
|
+
code: "export const translations = { key1: 'value1' }",
|
|
89
|
+
filename: setupTestFile('en_us.ts', { key1: 'value1' }),
|
|
90
|
+
options: [{ targetFileName: 'en_us.ts' }],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// カスタムファイル名: .js拡張子
|
|
94
|
+
{
|
|
95
|
+
code: "export const translations = { key1: 'value1' }",
|
|
96
|
+
filename: setupTestFile('custom.js', { key1: 'value1' }),
|
|
97
|
+
options: [{ targetFileName: 'custom.js' }],
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// カスタムファイル名: .tsx拡張子
|
|
101
|
+
{
|
|
102
|
+
code: "export const translations = { key1: 'value1' }",
|
|
103
|
+
filename: setupTestFile('custom.tsx', { key1: 'value1' }),
|
|
104
|
+
options: [{ targetFileName: 'custom.tsx' }],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// カスタムファイル名: .jsx拡張子
|
|
108
|
+
{
|
|
109
|
+
code: "export const translations = { key1: 'value1' }",
|
|
110
|
+
filename: setupTestFile('custom.jsx', { key1: 'value1' }),
|
|
111
|
+
options: [{ targetFileName: 'custom.jsx' }],
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// エスケープ: ダブルクォートを含む(シングルクォート文字列)
|
|
115
|
+
{
|
|
116
|
+
code: "export const translations = { key1: 'hogefuga\"piyo\".' }",
|
|
117
|
+
filename: setupTestFile('valid-escape-double.ts', { key1: 'hogefuga"piyo".' }),
|
|
118
|
+
options: defaultOptions,
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// エスケープ: シングルクォートを含む(ダブルクォート文字列)
|
|
122
|
+
{
|
|
123
|
+
code: "export const translations = { key1: \"hoge'fuga'.\" }",
|
|
124
|
+
filename: setupTestFile('valid-escape-single.ts', { key1: "hoge'fuga'." }),
|
|
125
|
+
options: defaultOptions,
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// エスケープ: バックスラッシュを含む
|
|
129
|
+
{
|
|
130
|
+
code: "export const translations = { key1: '円記号\\\\あり' }",
|
|
131
|
+
filename: setupTestFile('valid-escape-backslash.ts', { key1: '円記号\\あり' }),
|
|
132
|
+
options: defaultOptions,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// エスケープ: 改行を含む
|
|
136
|
+
{
|
|
137
|
+
code: "export const translations = { key1: '改行\\nあり' }",
|
|
138
|
+
filename: setupTestFile('valid-escape-newline.ts', { key1: '改行\nあり' }),
|
|
139
|
+
options: defaultOptions,
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// エスケープ: タブを含む
|
|
143
|
+
{
|
|
144
|
+
code: "export const translations = { key1: 'タブ\\tあり' }",
|
|
145
|
+
filename: setupTestFile('valid-escape-tab.ts', { key1: 'タブ\tあり' }),
|
|
146
|
+
options: defaultOptions,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// 特殊パターン: プレースホルダーを含む
|
|
150
|
+
{
|
|
151
|
+
code: "export const translations = { greeting: 'こんにちは、{name}さん' }",
|
|
152
|
+
filename: setupTestFile('valid-placeholder.ts', { greeting: 'こんにちは、{name}さん' }),
|
|
153
|
+
options: defaultOptions,
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// 特殊パターン: HTMLタグを含む
|
|
157
|
+
{
|
|
158
|
+
code: "export const translations = { message: 'これは改行<br></br>のテストです' }",
|
|
159
|
+
filename: setupTestFile('valid-br-tag.ts', { message: 'これは改行<br></br>のテストです' }),
|
|
160
|
+
options: defaultOptions,
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// 特殊パターン: 複数のプレースホルダーを含む
|
|
164
|
+
{
|
|
165
|
+
code: "export const translations = { count: '{count}件のうち{total}件を表示' }",
|
|
166
|
+
filename: setupTestFile('valid-multi-placeholder.ts', { count: '{count}件のうち{total}件を表示' }),
|
|
167
|
+
options: defaultOptions,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// 特殊パターン: 改行変数を含む
|
|
171
|
+
{
|
|
172
|
+
code: "export const translations = { error: 'エラーが発生しました。{br}再度お試しください。' }",
|
|
173
|
+
filename: setupTestFile('valid-br-variable.ts', { error: 'エラーが発生しました。{br}再度お試しください。' }),
|
|
174
|
+
options: defaultOptions,
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// 特殊パターン: 複合的なパターン
|
|
178
|
+
{
|
|
179
|
+
code: "export const translations = { complex: '{user}さん、<strong>重要</strong>なお知らせです。{br}詳細は{link}をご確認ください。' }",
|
|
180
|
+
filename: setupTestFile('valid-complex.ts', { complex: '{user}さん、<strong>重要</strong>なお知らせです。{br}詳細は{link}をご確認ください。' }),
|
|
181
|
+
options: defaultOptions,
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// 特殊パターン: 空のオブジェクト
|
|
185
|
+
{
|
|
186
|
+
code: "export const translations = {}",
|
|
187
|
+
filename: setupTestFile('valid-empty.ts', {}),
|
|
188
|
+
options: defaultOptions,
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// 複数のexport: 型とオブジェクトを両方export
|
|
192
|
+
{
|
|
193
|
+
code: "export type TranslationKeys = string; export const translations = { key1: 'value1' }",
|
|
194
|
+
filename: setupTestFile('valid-with-type.ts', { key1: 'value1' }),
|
|
195
|
+
options: defaultOptions,
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// 複数のexport: 型定義が複数ある場合
|
|
199
|
+
{
|
|
200
|
+
code: "export type Foo = string; export type Bar = number; export const translations = { key1: 'value1' }",
|
|
201
|
+
filename: setupTestFile('valid-with-multiple-types.ts', { key1: 'value1' }),
|
|
202
|
+
options: defaultOptions,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
|
|
206
|
+
invalid: [
|
|
207
|
+
// 複数のオブジェクトをexport
|
|
208
|
+
{
|
|
209
|
+
code: "export const a = { key1: 'value1' }; export const b = { key2: 'value2' }",
|
|
210
|
+
filename: createTestPath('ja.ts'),
|
|
211
|
+
options: defaultOptions,
|
|
212
|
+
errors: [{ messageId: 'multipleExports' }],
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// オブジェクトのexportがない
|
|
216
|
+
{
|
|
217
|
+
code: "const translations = { key1: 'value1' }",
|
|
218
|
+
filename: createTestPath('ja.ts'),
|
|
219
|
+
options: defaultOptions,
|
|
220
|
+
errors: [{ messageId: 'noExport' }],
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// オブジェクト以外をexport(文字列)
|
|
224
|
+
{
|
|
225
|
+
code: "export const translations = 'not an object'",
|
|
226
|
+
filename: createTestPath('ja.ts'),
|
|
227
|
+
options: defaultOptions,
|
|
228
|
+
errors: [{ messageId: 'noExport' }],
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// オブジェクト以外をexport(配列)
|
|
232
|
+
{
|
|
233
|
+
code: "export const translations = ['array']",
|
|
234
|
+
filename: createTestPath('ja.ts'),
|
|
235
|
+
options: defaultOptions,
|
|
236
|
+
errors: [{ messageId: 'noExport' }],
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// 値が文字列以外(数値)
|
|
240
|
+
{
|
|
241
|
+
code: "export const translations = { key1: 123 }",
|
|
242
|
+
filename: createTestPath('ja.ts'),
|
|
243
|
+
options: defaultOptions,
|
|
244
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// 値が文字列以外(真偽値)
|
|
248
|
+
{
|
|
249
|
+
code: "export const translations = { key1: true }",
|
|
250
|
+
filename: createTestPath('ja.ts'),
|
|
251
|
+
options: defaultOptions,
|
|
252
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// 値が文字列以外(null)
|
|
256
|
+
{
|
|
257
|
+
code: "export const translations = { key1: null }",
|
|
258
|
+
filename: createTestPath('ja.ts'),
|
|
259
|
+
options: defaultOptions,
|
|
260
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// ネストされたオブジェクト
|
|
264
|
+
{
|
|
265
|
+
code: "export const translations = { key1: { nested: 'value' } }",
|
|
266
|
+
filename: createTestPath('ja.ts'),
|
|
267
|
+
options: defaultOptions,
|
|
268
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// スプレッド構文
|
|
272
|
+
{
|
|
273
|
+
code: "const base = { key1: 'value1' }; export const translations = { ...base, key2: 'value2' }",
|
|
274
|
+
filename: createTestPath('ja.ts'),
|
|
275
|
+
options: defaultOptions,
|
|
276
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// テンプレートリテラルに式を含む
|
|
280
|
+
{
|
|
281
|
+
code: 'export const translations = { key1: `value${1}` }',
|
|
282
|
+
filename: createTestPath('ja.ts'),
|
|
283
|
+
options: defaultOptions,
|
|
284
|
+
errors: [{ messageId: 'invalidValue' }],
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
// JSONファイルが存在しない
|
|
288
|
+
{
|
|
289
|
+
code: "export const translations = { key1: 'value1' }",
|
|
290
|
+
filename: createTestPath('ja.ts'),
|
|
291
|
+
options: defaultOptions,
|
|
292
|
+
errors: [{ messageId: 'notSync' }],
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// ja.ts と ja.json が不一致
|
|
296
|
+
{
|
|
297
|
+
code: "export const translations = { key1: 'new value' }",
|
|
298
|
+
filename: (() => {
|
|
299
|
+
const tsPath = path.join(tmpDir, 'sub1', 'ja.ts')
|
|
300
|
+
const jsonPath = path.join(tmpDir, 'sub1', 'ja.json')
|
|
301
|
+
fs.mkdirSync(path.dirname(tsPath), { recursive: true })
|
|
302
|
+
fs.writeFileSync(jsonPath, '{\n "key1": "old value"\n}\n', 'utf8')
|
|
303
|
+
return tsPath
|
|
304
|
+
})(),
|
|
305
|
+
options: defaultOptions,
|
|
306
|
+
errors: [{ messageId: 'notSync' }],
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
})
|