Repository: kamiya777/qiita_app
Pull request #16: Feature/localize ja
Author: login: kamiya777
Created: 2025-03-13T11:55:24Z | Merged: 2025-03-24T08:41:02Z

## 概要
localizeの対応
<!-- このPRの目的と概要を簡潔に説明してください -->

## 関連issue
close #10 
<!--
もし、IssueからこのPRを作成した場合、そのIssue番号を記載してください
記載するとき、このPRがマージされたら自動的にIssueがクローズされる様に記載してください
参考: https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue
-->

## やったこと
- .stringsファイルの作成
- localizeの対応

<!-- 具体的に実装した内容を記載してください -->

## やらないこと（このプルリクエストのスコープ外とすること）

## 動作確認方法

<!-- 今回の実装が正しいかどうかをどう確認したのか記載してください -->

## スクリーンショット
![スクリーンショット 2025-03-13 20 59 07](https://github.com/user-attachments/assets/8b05cb1c-2b9d-4282-97f4-04eeffe06638)
![スクリーンショット 2025-03-13 20 59 34](https://github.com/user-attachments/assets/d21beef9-8825-471f-830b-312524342ce1)
![スクリーンショット 2025-03-13 21 00 00](https://github.com/user-attachments/assets/80c87bdb-0ec5-4905-b38d-8617a402742c)


<!-- アプリ画面上に変化が見える場合、スクショまたは動画を掲載してください -->

## レビュー観点

<!-- レビューアに特に確認してほしい箇所があれば記載してください -->


Linked issue #10: 日本語のローカライズ対応
## 概要

画面のタイトル部分など、ローカライズ対応できる箇所をローカライズする
今回は日本語だけ対応する

Diff:
diff --git a/.gitignore b/.gitignore
index 5df5e39..2e434ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,6 @@ DerivedData/
 
 # ビルド出力ディレクトリ
 build/
+
+# Xcodeのブレークポイント設定ファイル
+Breakpoints_v2.xcbkptlist
diff --git a/QiitaApp.xcodeproj/project.pbxproj b/QiitaApp.xcodeproj/project.pbxproj
index 2d3ae7e..507cfa5 100644
--- a/QiitaApp.xcodeproj/project.pbxproj
+++ b/QiitaApp.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		BF0EEF862D82FA7600238E0C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BF0EEF842D82FA7600238E0C /* Localizable.strings */; };
 		BF3FA2202D54C98500B21B9C /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3FA21F2D54C98500B21B9C /* Item.swift */; };
 		BF3FA2222D54C9A500B21B9C /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3FA2212D54C9A500B21B9C /* User.swift */; };
 		BF3FA2242D54C9E000B21B9C /* ApiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3FA2232D54C9E000B21B9C /* ApiItem.swift */; };
@@ -15,18 +16,20 @@
 		BF7A06372D44A664009F6CB3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF7A06362D44A664009F6CB3 /* Assets.xcassets */; };
 		BF7A063A2D44A664009F6CB3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF7A06392D44A664009F6CB3 /* Preview Assets.xcassets */; };
 		BF7A06482D44A857009F6CB3 /* MyPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A06472D44A857009F6CB3 /* MyPageViewModel.swift */; };
-		BF7A064A2D44A876009F6CB3 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A06492D44A876009F6CB3 /* ContentViewModel.swift */; };
+		BF7A064A2D44A876009F6CB3 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A06492D44A876009F6CB3 /* LoginViewModel.swift */; };
 		BF7A064C2D44A893009F6CB3 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A064B2D44A893009F6CB3 /* SearchViewModel.swift */; };
-		BF7A064E2D44A8F4009F6CB3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A064D2D44A8F4009F6CB3 /* ContentView.swift */; };
+		BF7A064E2D44A8F4009F6CB3 /* LogintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A064D2D44A8F4009F6CB3 /* LogintView.swift */; };
 		BF7A06502D44A953009F6CB3 /* MyPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A064F2D44A953009F6CB3 /* MyPageView.swift */; };
 		BF7A06522D44A970009F6CB3 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A06512D44A970009F6CB3 /* SearchView.swift */; };
 		BF7A06542D44A990009F6CB3 /* SearchResultView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7A06532D44A990009F6CB3 /* SearchResultView.swift */; };
 		BF873A792D44BCC900CE8CFE /* ItemDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF873A782D44BCC900CE8CFE /* ItemDetailView.swift */; };
 		BF873A7B2D44BEAE00CE8CFE /* ItemDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF873A7A2D44BEAE00CE8CFE /* ItemDetailViewModel.swift */; };
 		BF873A7D2D48A90300CE8CFE /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF873A7C2D48A90300CE8CFE /* MainTabView.swift */; };
+		BFF40B292D59A5DE00E2835D /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF40B282D59A5DE00E2835D /* ApiService.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
+		BF0EEF872D82FA8300238E0C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
 		BF3FA21F2D54C98500B21B9C /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
 		BF3FA2212D54C9A500B21B9C /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
 		BF3FA2232D54C9E000B21B9C /* ApiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiItem.swift; sourceTree = "<group>"; };
@@ -36,15 +39,16 @@
 		BF7A06362D44A664009F6CB3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		BF7A06392D44A664009F6CB3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
 		BF7A06472D44A857009F6CB3 /* MyPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageViewModel.swift; sourceTree = "<group>"; };
-		BF7A06492D44A876009F6CB3 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
+		BF7A06492D44A876009F6CB3 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
 		BF7A064B2D44A893009F6CB3 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
-		BF7A064D2D44A8F4009F6CB3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		BF7A064D2D44A8F4009F6CB3 /* LogintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogintView.swift; sourceTree = "<group>"; };
 		BF7A064F2D44A953009F6CB3 /* MyPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPageView.swift; sourceTree = "<group>"; };
 		BF7A06512D44A970009F6CB3 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
 		BF7A06532D44A990009F6CB3 /* SearchResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultView.swift; sourceTree = "<group>"; };
 		BF873A782D44BCC900CE8CFE /* ItemDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailView.swift; sourceTree = "<group>"; };
 		BF873A7A2D44BEAE00CE8CFE /* ItemDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDetailViewModel.swift; sourceTree = "<group>"; };
 		BF873A7C2D48A90300CE8CFE /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
+		BFF40B282D59A5DE00E2835D /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -77,12 +81,14 @@
 		BF7A06312D44A663009F6CB3 /* QiitaApp */ = {
 			isa = PBXGroup;
 			children = (
+				BFF40B2A2D59A68F00E2835D /* Services */,
 				BF7A06422D44A795009F6CB3 /* Models */,
 				BF7A06412D44A771009F6CB3 /* ViewModels */,
 				BF7A06402D44A752009F6CB3 /* Views */,
 				BF7A06322D44A663009F6CB3 /* QiitaAppApp.swift */,
 				BF7A06362D44A664009F6CB3 /* Assets.xcassets */,
 				BF7A06382D44A664009F6CB3 /* Preview Content */,
+				BF0EEF842D82FA7600238E0C /* Localizable.strings */,
 			);
 			path = QiitaApp;
 			sourceTree = "<group>";
@@ -98,7 +104,7 @@
 		BF7A06402D44A752009F6CB3 /* Views */ = {
 			isa = PBXGroup;
 			children = (
-				BF7A064D2D44A8F4009F6CB3 /* ContentView.swift */,
+				BF7A064D2D44A8F4009F6CB3 /* LogintView.swift */,
 				BF7A064F2D44A953009F6CB3 /* MyPageView.swift */,
 				BF7A06512D44A970009F6CB3 /* SearchView.swift */,
 				BF7A06532D44A990009F6CB3 /* SearchResultView.swift */,
@@ -112,7 +118,7 @@
 			isa = PBXGroup;
 			children = (
 				BF7A06472D44A857009F6CB3 /* MyPageViewModel.swift */,
-				BF7A06492D44A876009F6CB3 /* ContentViewModel.swift */,
+				BF7A06492D44A876009F6CB3 /* LoginViewModel.swift */,
 				BF7A064B2D44A893009F6CB3 /* SearchViewModel.swift */,
 				BF873A7A2D44BEAE00CE8CFE /* ItemDetailViewModel.swift */,
 			);
@@ -130,6 +136,14 @@
 			path = Models;
 			sourceTree = "<group>";
 		};
+		BFF40B2A2D59A68F00E2835D /* Services */ = {
+			isa = PBXGroup;
+			children = (
+				BFF40B282D59A5DE00E2835D /* ApiService.swift */,
+			);
+			path = Services;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -167,11 +181,10 @@
 			};
 			buildConfigurationList = BF7A062A2D44A663009F6CB3 /* Build configuration list for PBXProject "QiitaApp" */;
 			compatibilityVersion = "Xcode 14.0";
-			developmentRegion = en;
+			developmentRegion = ja;
 			hasScannedForEncodings = 0;
 			knownRegions = (
-				en,
-				Base,
+				ja,
 			);
 			mainGroup = BF7A06262D44A663009F6CB3;
 			productRefGroup = BF7A06302D44A663009F6CB3 /* Products */;
@@ -189,6 +202,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				BF7A063A2D44A664009F6CB3 /* Preview Assets.xcassets in Resources */,
+				BF0EEF862D82FA7600238E0C /* Localizable.strings in Resources */,
 				BF7A06372D44A664009F6CB3 /* Assets.xcassets in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -206,12 +220,13 @@
 				BF7A06502D44A953009F6CB3 /* MyPageView.swift in Sources */,
 				BF873A792D44BCC900CE8CFE /* ItemDetailView.swift in Sources */,
 				BF3FA2202D54C98500B21B9C /* Item.swift in Sources */,
-				BF7A064E2D44A8F4009F6CB3 /* ContentView.swift in Sources */,
+				BFF40B292D59A5DE00E2835D /* ApiService.swift in Sources */,
+				BF7A064E2D44A8F4009F6CB3 /* LogintView.swift in Sources */,
 				BF873A7D2D48A90300CE8CFE /* MainTabView.swift in Sources */,
 				BF7A06332D44A663009F6CB3 /* QiitaAppApp.swift in Sources */,
 				BF3FA2242D54C9E000B21B9C /* ApiItem.swift in Sources */,
 				BF7A06482D44A857009F6CB3 /* MyPageViewModel.swift in Sources */,
-				BF7A064A2D44A876009F6CB3 /* ContentViewModel.swift in Sources */,
+				BF7A064A2D44A876009F6CB3 /* LoginViewModel.swift in Sources */,
 				BF3FA2262D54CA0700B21B9C /* ApiUser.swift in Sources */,
 				BF873A7B2D44BEAE00CE8CFE /* ItemDetailViewModel.swift in Sources */,
 				BF7A06542D44A990009F6CB3 /* SearchResultView.swift in Sources */,
@@ -220,6 +235,17 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXVariantGroup section */
+		BF0EEF842D82FA7600238E0C /* Localizable.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				BF0EEF872D82FA8300238E0C /* ja */,
+			);
+			name = Localizable.strings;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
 /* Begin XCBuildConfiguration section */
 		BF7A063B2D44A664009F6CB3 /* Debug */ = {
 			isa = XCBuildConfiguration;
diff --git a/QiitaApp/QiitaAppApp.swift b/QiitaApp/QiitaAppApp.swift
index dc8ae21..961201d 100644
--- a/QiitaApp/QiitaAppApp.swift
+++ b/QiitaApp/QiitaAppApp.swift
@@ -11,7 +11,7 @@ import SwiftUI
 struct QiitaAppApp: App {
     var body: some Scene {
         WindowGroup {
-            ContentView(isVisible: true)
+            LoginView(isVisible: true)
         }
     }
 }
diff --git a/QiitaApp/Services/ApiService.swift b/QiitaApp/Services/ApiService.swift
new file mode 100644
index 0000000..95879ad
--- /dev/null
+++ b/QiitaApp/Services/ApiService.swift
@@ -0,0 +1,59 @@
+//
+//  ApiService.swift
+//  QiitaApp
+//
+//  Created by kamiya on 2025/02/10.
+//
+
+import Foundation
+import Combine
+
+class ApiService {
+    static let shared = ApiService()
+    
+    private init() {}
+    
+    // ユーザー情報を取得する
+    func fetchUserData(accessToken: String) -> AnyPublisher<ApiUser, Error> {
+        guard let url = URL(string: "https://qiita.com/api/v2/authenticated_user") else {
+            return Fail(error: NSError(domain: "Invalid URL", code: 400, userInfo: nil)).eraseToAnyPublisher()
+        }
+        
+        var request = URLRequest(url: url)
+        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
+        
+        return URLSession.shared.dataTaskPublisher(for: request)
+            .map { $0.data }
+            .decode(type: ApiUser.self, decoder: JSONDecoder())
+            .eraseToAnyPublisher()
+    }
+    
+    // アイテムを検索する
+    func searchItems(query: String) -> AnyPublisher<[ApiItem], Error> {
+        guard !query.isEmpty, let url = URL(string: "https://qiita.com/api/v2/items?query=\(query)") else {
+            return Fail(error: NSError(domain: "Invalid Input", code: 400, userInfo: nil)).eraseToAnyPublisher()
+        }
+        
+        var request = URLRequest(url: url)
+        request.httpMethod = "GET"
+        
+        return URLSession.shared.dataTaskPublisher(for: request)
+            .map { $0.data }
+            .decode(type: [ApiItem].self, decoder: JSONDecoder())
+            .eraseToAnyPublisher()
+    }
+    
+    // アイテムの詳細を取得する
+    func fetchItemDetails(itemId: String) -> AnyPublisher<ApiItem, Error> {
+        let urlString = "https://qiita.com/api/v2/items/\(itemId)"
+        
+        guard let url = URL(string: urlString) else {
+            return Fail(error: NSError(domain: "Invalid URL", code: 400, userInfo: nil)).eraseToAnyPublisher()
+        }
+        
+        return URLSession.shared.dataTaskPublisher(for: url)
+            .map { $0.data }
+            .decode(type: ApiItem.self, decoder: JSONDecoder())
+            .eraseToAnyPublisher()
+    }
+}
diff --git a/QiitaApp/ViewModels/ContentViewModel.swift b/QiitaApp/ViewModels/ContentViewModel.swift
deleted file mode 100644
index fbfeec3..0000000
--- a/QiitaApp/ViewModels/ContentViewModel.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-//
-//  ContentViewModel.swift
-//  QiitaApp
-//
-//  Created by kamiya on 2025/01/25.
-//
-
-import SwiftUI
-
-
-class ContentViewModel: ObservableObject {
-    @Published var accessToken: String = ""
-    
-    private let apiBaseURL = "https://qiita.com/api/v2/authenticated_user"
-    
-    // ログインアクション
-    func loginAction(completion: @escaping (Result<Void, Error>) -> Void) {
-        guard !accessToken.isEmpty else {
-            completion(.failure(NSError(domain: "Invalid Input", code: 400, userInfo: [NSLocalizedDescriptionKey: "アクセストークンが入力されていません!"])))
-            return
-        }
-        
-        var request = URLRequest(url: URL(string: apiBaseURL)!)
-        request.httpMethod = "GET"
-        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
-        
-        let task = URLSession.shared.dataTask(with: request) { data, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    completion(.failure(error))
-                }
-                return
-            }
-            
-            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
-                // 認証成功
-                DispatchQueue.main.async {
-                    completion(.success(()))
-                }
-            } else {
-                // 認証失敗
-                let error = NSError(domain: "Authentication Failed", code: 401, userInfo: [NSLocalizedDescriptionKey: "アクセストークンが間違っています。"])
-                DispatchQueue.main.async {
-                    completion(.failure(error))
-                }
-            }
-        }
-        
-        task.resume()
-    }
-}
-
diff --git a/QiitaApp/ViewModels/ItemDetailViewModel.swift b/QiitaApp/ViewModels/ItemDetailViewModel.swift
index 285bc8a..29ba1ee 100644
--- a/QiitaApp/ViewModels/ItemDetailViewModel.swift
+++ b/QiitaApp/ViewModels/ItemDetailViewModel.swift
@@ -11,48 +11,27 @@ import Combine
 class ItemDetailViewModel: ObservableObject {
     @Published var item: Item?
     @Published var isLoading: Bool = false
-    @Published var errorMessage: String?
-    
+    @Published var errorMessage: LocalizedStringKey?
+
+    private var cancellables = Set<AnyCancellable>()
+
+    // アイテム詳細を取得
     func fetchItemDetails(itemId: String) {
         isLoading = true
-        let urlString = "https://qiita.com/api/v2/items/\(itemId)"
-        
-        guard let url = URL(string: urlString) else {
-            errorMessage = "Invalid URL"
-            isLoading = false
-            return
-        }
         
-        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    self?.errorMessage = error.localizedDescription
-                }
-                return
-            }
-            
-            guard let data = data else {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "No data received"
-                }
-                return
-            }
-            
-            do {
-                // JSONをデコードしてItemを作成
-                let decodedApiItem = try JSONDecoder().decode(ApiItem.self, from: data)
-                
-                // UI関連の処理だけをメインスレッドで行う
-                DispatchQueue.main.async {
-                    self?.isLoading = false
-                    self?.item = Item(from: decodedApiItem)
-                }
-            } catch {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "Failed to decode data"
+        ApiService.shared.fetchItemDetails(itemId: itemId)
+            .receive(on: DispatchQueue.main)
+            .sink(receiveCompletion: { [weak self] completion in
+                switch completion {
+                case .failure(let error):
+                    self?.errorMessage = LocalizedStringKey("errorOccurred \(error.localizedDescription)")
+                case .finished:
+                    break
                 }
-            }
-        }
-        task.resume()
+                self?.isLoading = false
+            }, receiveValue: { [weak self] apiItem in
+                self?.item = Item(from: apiItem)
+            })
+            .store(in: &cancellables)
     }
 }
diff --git a/QiitaApp/ViewModels/LoginViewModel.swift b/QiitaApp/ViewModels/LoginViewModel.swift
new file mode 100644
index 0000000..3c7ab74
--- /dev/null
+++ b/QiitaApp/ViewModels/LoginViewModel.swift
@@ -0,0 +1,45 @@
+//
+//  LoginViewModel.swift
+//  QiitaApp
+//
+//  Created by kamiya on 2025/01/25.
+//
+
+import SwiftUI
+import Combine
+
+class LoginViewModel: ObservableObject {
+    @Published var accessToken: String = ""
+    @Published var isLoggedIn: Bool = false
+    @Published var errorMessage: LocalizedStringKey?
+    @Published var showAlert: Bool = false
+    
+    private var cancellables = Set<AnyCancellable>()
+    
+    // ログインアクション
+    func loginAction() {
+            guard !accessToken.isEmpty else {
+                errorMessage = "emptyAccessToken"
+                showAlert = true
+                return
+            }
+
+            ApiService.shared.fetchUserData(accessToken: accessToken)
+                .receive(on: DispatchQueue.main)
+                .sink(receiveCompletion: { completion in
+                    switch completion {
+                    case .failure(let error):
+                        self.isLoggedIn = false
+                        self.errorMessage = LocalizedStringKey(error.localizedDescription)
+                        self.showAlert = true
+                    case .finished:
+                        break
+                    }
+                }, receiveValue: { _ in
+                    self.isLoggedIn = true
+                    self.errorMessage = nil
+                    UserDefaults.standard.set(self.accessToken, forKey: "accessToken")
+                })
+                .store(in: &cancellables)
+        }
+    }
diff --git a/QiitaApp/ViewModels/SearchViewModel.swift b/QiitaApp/ViewModels/SearchViewModel.swift
index 75d5ce8..34d760a 100644
--- a/QiitaApp/ViewModels/SearchViewModel.swift
+++ b/QiitaApp/ViewModels/SearchViewModel.swift
@@ -6,13 +6,14 @@
 //
 
 import SwiftUI
+import Combine
 
 class SearchViewModel: ObservableObject {
     @Published var searchResults: [Item] = []
     @Published var isLoading: Bool = false
-    @Published var errorMessage: String? = nil
+    @Published var errorMessage: LocalizedStringKey? = nil
     
-    private let apiBaseURL = "https://qiita.com/api/v2/items"
+    private var cancellables = Set<AnyCancellable>()
     
     // 検索処理
     func searchItems(query: String) {
@@ -24,34 +25,19 @@ class SearchViewModel: ObservableObject {
         self.isLoading = true
         self.errorMessage = nil
         
-        var request = URLRequest(url: URL(string: apiBaseURL + "?query=\(query)")!)
-        request.httpMethod = "GET"
-        
-        let task = URLSession.shared.dataTask(with: request) { data, response, error in
-            DispatchQueue.main.async {
-                // エラーハンドリング
-                if let error = error {
-                    self.errorMessage = "エラーが発生しました: \(error.localizedDescription)"
-                    self.isLoading = false
-                    return
-                }
-                
-                // レスポンス処理
-                if let data = data {
-                    do {
-                        let decodedResponse = try JSONDecoder().decode([ApiItem].self, from: data)
-                        self.searchResults = decodedResponse.map { Item(from: $0) }
-                    } catch {
-                        self.errorMessage = "データの解析に失敗しました"
-                    }
+        ApiService.shared.searchItems(query: query)
+            .receive(on: DispatchQueue.main)
+            .sink(receiveCompletion: { [weak self] completion in
+                switch completion {
+                case .failure:
+                    self?.errorMessage = LocalizedStringKey("genericError")
+                case .finished:
+                    break
                 }
-                
-                self.isLoading = false
-            }
-        }
-        
-        task.resume()
+                self?.isLoading = false
+            }, receiveValue: { [weak self] apiItems in
+                self?.searchResults = apiItems.map { Item(from: $0) }
+            })
+            .store(in: &cancellables)
     }
 }
-
-
diff --git a/QiitaApp/Views/ContentView.swift b/QiitaApp/Views/ContentView.swift
deleted file mode 100644
index 60bc4e4..0000000
--- a/QiitaApp/Views/ContentView.swift
+++ /dev/null
@@ -1,69 +0,0 @@
-//
-//  ContentView.swift
-//  QiitaApp
-//
-//  Created by kamiya on 2025/01/25.
-//
-
-import SwiftUI
-
-struct ContentView: View {
-    @ObservedObject var viewModel = ContentViewModel()
-    
-    @State private var isLoggedIn = false
-    @State private var isActive = false
-    @State private var showAlert = false
-    @State private var alertMessage = ""
-    
-    var isVisible: Bool
-    
-    var body: some View {
-        NavigationStack {
-            VStack {
-                TextField("アクセストークン", text: $viewModel.accessToken)
-                    .padding()
-                    .textFieldStyle(RoundedBorderTextFieldStyle())
-                    .frame(width: 300)
-                
-                Button(action: {
-                    viewModel.loginAction { result in
-                        switch result {
-                        case .success:
-                            UserDefaults.standard.set(viewModel.accessToken, forKey: "accessToken")
-                            isLoggedIn = true
-                            isActive = true
-                        case .failure(let error):
-                            alertMessage = error.localizedDescription
-                            showAlert = true
-                        }
-                    }
-                }) {
-                    Text("ログイン")
-                        .foregroundColor(.white)
-                        .frame(width: 200, height: 50)
-                        .background(Color.blue)
-                        .cornerRadius(10)
-                }
-                .padding(.top, 20)
-                if isVisible {
-                    Button(action: {
-                        isActive = true
-                    }) {
-                        Text("ログインせずに利用する")
-                            .foregroundColor(.blue)
-                            .padding()
-                    }
-                    .padding(.top, 10)
-                }
-                Spacer()
-            }
-            .padding()
-            .alert(isPresented: $showAlert) {
-                Alert(title: Text("エラー"), message: Text(alertMessage), dismissButton: .default(Text("OK")))
-            }
-            .navigationDestination(isPresented: $isActive) {
-                MainTabView(isLoggedIn: $isLoggedIn)
-            }
-        }
-    }
-}
diff --git a/QiitaApp/Views/ItemDetailView.swift b/QiitaApp/Views/ItemDetailView.swift
index 8aa1a4c..253a160 100644
--- a/QiitaApp/Views/ItemDetailView.swift
+++ b/QiitaApp/Views/ItemDetailView.swift
@@ -14,7 +14,7 @@ struct ItemDetailView: View {
     var body: some View {
         VStack {
             if viewModel.isLoading {
-                ProgressView("Loading...")
+                ProgressView(LocalizedStringKey("loadingText"))
                     .progressViewStyle(CircularProgressViewStyle())
             } else if let item = viewModel.item {
                 Text(item.title)
@@ -26,14 +26,14 @@ struct ItemDetailView: View {
                         .padding()
                 }
             } else if let errorMessage = viewModel.errorMessage {
-                Text("Error: \(errorMessage)")
+                Text(errorMessage)
                     .foregroundColor(.red)
                     .padding()
             }
             
             Spacer()
         }
-        .navigationTitle("記事詳細")
+        .navigationTitle(LocalizedStringKey("itemDetailViewTitle"))
         .onAppear {
             viewModel.fetchItemDetails(itemId: itemId)
         }
diff --git a/QiitaApp/Views/LogintView.swift b/QiitaApp/Views/LogintView.swift
new file mode 100644
index 0000000..1b234ee
--- /dev/null
+++ b/QiitaApp/Views/LogintView.swift
@@ -0,0 +1,58 @@
+//
+//  LoginView.swift
+//  QiitaApp
+//
+//  Created by kamiya on 2025/01/25.
+//
+
+import SwiftUI
+
+struct LoginView: View {
+    @ObservedObject var viewModel = LoginViewModel()
+    @State private var isActive = false
+    
+    var isVisible: Bool
+    
+    var body: some View {
+        NavigationStack {
+            VStack {
+                TextField(LocalizedStringKey("loginViewAccessTokenPlaceHolder"), text: $viewModel.accessToken)
+                    .padding()
+                    .textFieldStyle(RoundedBorderTextFieldStyle())
+                    .frame(width: 300)
+                
+                Button(action: {
+                    viewModel.loginAction()
+                }) {
+                    Text(LocalizedStringKey("loginViewLoginButton"))
+                        .foregroundColor(.white)
+                        .frame(width: 200, height: 50)
+                        .background(Color.blue)
+                        .cornerRadius(10)
+                }
+                .padding(.top, 20)
+                if isVisible {
+                    Button(action: {
+                        isActive = true
+                    }) {
+                        Text(LocalizedStringKey("loginViewUseWithoutLoginText"))
+                            .foregroundColor(.blue)
+                            .padding()
+                    }
+                    .padding(.top, 10)
+                }
+                Spacer()
+            }
+            .padding()
+            .alert(isPresented: $viewModel.showAlert) {
+                Alert(title: Text(LocalizedStringKey("errorText")), message: Text(viewModel.errorMessage ?? LocalizedStringKey("loginViewUnknownError")), dismissButton: .default(Text(LocalizedStringKey("okDismissButton"))))
+            }
+            .navigationDestination(isPresented: $viewModel.isLoggedIn) {
+                MainTabView(isLoggedIn: $viewModel.isLoggedIn)
+            }
+            .navigationDestination(isPresented: $isActive) {
+                MainTabView(isLoggedIn: .constant(false))
+            }
+        }
+    }
+}
diff --git a/QiitaApp/Views/MainTabView.swift b/QiitaApp/Views/MainTabView.swift
index b2d8206..be39bb7 100644
--- a/QiitaApp/Views/MainTabView.swift
+++ b/QiitaApp/Views/MainTabView.swift
@@ -14,12 +14,12 @@ struct MainTabView: View {
         TabView {
             SearchView()
                 .tabItem {
-                    Label("検索", systemImage: "magnifyingglass")
+                    Label(LocalizedStringKey("searchViewTitle"), systemImage: "magnifyingglass")
                 }
             
             MyPageView(isLoggedIn: $isLoggedIn)
                 .tabItem {
-                    Label("マイページ", systemImage: "person.circle")
+                    Label(LocalizedStringKey("myPageViewTitle"), systemImage: "person.circle")
                 }
         }
     }
diff --git a/QiitaApp/Views/MyPageView.swift b/QiitaApp/Views/MyPageView.swift
index 10b0dc6..031a230 100644
--- a/QiitaApp/Views/MyPageView.swift
+++ b/QiitaApp/Views/MyPageView.swift
@@ -19,13 +19,13 @@ struct MyPageView: View {
                 if viewModel.hasError {
                     // エラー発生時の表示（ユーザー情報取得失敗）
                     VStack(alignment: .leading, spacing: 10) {
-                        Text("ユーザー名: -")
+                        Text(LocalizedStringKey("myPageViewUser"))
                             .font(.headline)
                         Spacer().frame(height: 20)
-                        Text("フォローしている数: -")
-                        Text("フォローされている数: -")
-                        Text("Qiitaに公開している記事の数: -")
-                        Text("移住地: -")
+                        Text(LocalizedStringKey("myPageViewNumberOfFollowers"))
+                        Text(LocalizedStringKey("myPageViewNumberOfFollowees"))
+                        Text(LocalizedStringKey("myPageViewNumberOfQiitaArticles"))
+                        Text(LocalizedStringKey("myPageViewLocation"))
                     }
                     .padding(.leading)
                     Spacer().frame(height: 40)
@@ -36,7 +36,7 @@ struct MyPageView: View {
                             viewModel.fetchUserData(accessToken: token)
                         }
                     }) {
-                        Text("再読み込み")
+                        Text(LocalizedStringKey("myPageViewReload"))
                             .foregroundColor(.blue)
                             .padding()
                             .background(Capsule().strokeBorder(Color.blue, lineWidth: 2))
@@ -44,18 +44,18 @@ struct MyPageView: View {
                 } else if let user = viewModel.user {
                     // 正常にユーザー情報が取得できた場合
                     VStack(alignment: .leading, spacing: 10) {
-                        Text("ユーザー名: \(user.name)")
+                        Text(LocalizedStringKey("myPageViewUserName \(user.name)"))
                             .font(.headline)
                         Spacer().frame(height: 20)
-                        Text("フォローしている数: \(user.followeesCount)")
-                        Text("フォローされている数: \(user.followersCount)")
-                        Text("Qiitaに公開している記事の数: \(user.itemsCount)")
-                        Text("移住地: \(user.location)")
+                        Text(LocalizedStringKey("myPageViewNumberOfFollowersCount \(user.followeesCount)"))
+                        Text(LocalizedStringKey("myPageViewNumberOfFolloweesCount \(user.followersCount)"))
+                        Text(LocalizedStringKey("myPageViewNumberOfQiitaArticlesCount \(user.itemsCount)"))
+                        Text(LocalizedStringKey("myPageViewLocationString \(user.location)"))
                     }
                     .padding(.leading)
                 } else {
                     // 読み込み中の場合
-                    ProgressView("読み込み中...")
+                    ProgressView(LocalizedStringKey("loadingText"))
                         .padding()
                 }
                 
@@ -66,7 +66,7 @@ struct MyPageView: View {
                         viewModel.logout()
                         isLoggedIn = false
                     }) {
-                        Text("ログアウト")
+                        Text(LocalizedStringKey("myPageViewLogout"))
                             .foregroundColor(.red)
                             .padding()
                             .background(Capsule().strokeBorder(Color.red, lineWidth: 2))
@@ -75,7 +75,7 @@ struct MyPageView: View {
                 }
                 .padding(.horizontal)
             } else {
-                ContentView(isVisible: isLoggedIn)
+                LogintView(isVisible: isLoggedIn)
             }
             
             Spacer()
@@ -89,9 +89,9 @@ struct MyPageView: View {
         .alert(isPresented: $showAlert) {
             // アラートを表示する条件を指定
             Alert(
-                title: Text("エラー"),
-                message: Text("ユーザー情報の取得に失敗しました。再度お試しください。"),
-                dismissButton: .default(Text("OK"))
+                title: Text(LocalizedStringKey("errorText")),
+                message: Text(LocalizedStringKey("myPageViewGetUserError")),
+                dismissButton: .default(Text(LocalizedStringKey("okDismissButton")))
             )
         }
         // hasErrorがtrueの時アラート表示する
diff --git a/QiitaApp/Views/SearchResultView.swift b/QiitaApp/Views/SearchResultView.swift
index 62976e9..c6ace02 100644
--- a/QiitaApp/Views/SearchResultView.swift
+++ b/QiitaApp/Views/SearchResultView.swift
@@ -21,6 +21,6 @@ struct SearchResultView: View {
                 .padding(.vertical, 5)
             }
         }
-        .navigationTitle("検索結果一覧")
+        .navigationTitle(LocalizedStringKey("searchResultViewListTitle"))
     }
 }
diff --git a/QiitaApp/Views/SearchView.swift b/QiitaApp/Views/SearchView.swift
index f697be5..dec197c 100644
--- a/QiitaApp/Views/SearchView.swift
+++ b/QiitaApp/Views/SearchView.swift
@@ -18,7 +18,7 @@ struct SearchView: View {
         VStack {
             HStack {
                 // フリーワード入力欄
-                TextField("フリーワード", text: $searchText)
+                TextField(LocalizedStringKey("searchViewFreeWordPlaceHolder"), text: $searchText)
                     .padding()
                     .textFieldStyle(RoundedBorderTextFieldStyle())
                     .frame(width: 250)
@@ -29,7 +29,7 @@ struct SearchView: View {
                     isSearching = true
                     isActive = true
                 }) {
-                    Text("検索")
+                    Text(LocalizedStringKey("searchViewSearchButton"))
                         .foregroundColor(.white)
                         .padding(.horizontal, 15)
                         .padding(.vertical, 12)
@@ -40,7 +40,7 @@ struct SearchView: View {
             .padding()
             
             if viewModel.isLoading {
-                ProgressView("検索中...")
+                ProgressView(LocalizedStringKey("searchViewSearching"))
                     .progressViewStyle(CircularProgressViewStyle())
             } else if let errorMessage = viewModel.errorMessage {
                 Text(errorMessage)
@@ -60,4 +60,3 @@ struct SearchView: View {
         }
     }
 }
-
diff --git a/QiitaApp/ja.lproj/Localizable.strings b/QiitaApp/ja.lproj/Localizable.strings
new file mode 100644
index 0000000..af59a2f
--- /dev/null
+++ b/QiitaApp/ja.lproj/Localizable.strings
@@ -0,0 +1,48 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"errorText" = "エラー";
+"loadingText" = "読み込み中...";
+"okDismissButton" = "OK";
+
+
+"loginViewAccessTokenPlaceHolder" = "アクセストークン";
+"loginViewLoginButton" = "ログイン";
+"loginViewUseWithoutLoginText" = "ログインせずに利用する";
+"loginViewUnknownError" = "不明なエラー";
+
+"myPageViewUser" = "ユーザー名: -";
+"myPageViewNumberOfFollowers" = "フォローしている数: -";
+"myPageViewNumberOfFollowees" = "フォローされている数: -";
+"myPageViewNumberOfQiitaArticles" = "Qiitaに公開している記事の数: -";
+"myPageViewLocation" = "移住地: -";
+"myPageViewReload" = "再読み込み";
+"myPageViewUserName" = "ユーザー名: %@";
+"myPageViewNumberOfFollowersCount" = "フォローしている数: %d";
+"myPageViewNumberOfFolloweesCount" = "フォローされている数: %d";
+"myPageViewNumberOfQiitaArticlesCount" = "Qiitaに公開している記事の数: %d";
+"myPageViewLocationString" = "移住地: %@";
+
+"myPageViewLogout" = "ログアウト";
+"myPageViewGetUserError" = "ユーザー情報の取得に失敗しました。再度お試しください。";
+
+"searchViewFreeWordPlaceHolder" = "フリーワード";
+"searchViewSearchButton" = "検索";
+"searchViewSearching" = "検索中...";
+
+"searchResultViewListTitle" = "検索結果一覧";
+
+"itemDetailViewTitle" = "記事詳細";
+
+"myPageViewTitle" = "マイページ";
+"searchViewTitle" = "検索";
+
+
+"genericError" = "エラーが発生しました";
+"errorOccurred" = "エラーが発生しました: %@";
+"emptyAccessToken" = "アクセストークンが入力されていません!";


Reviews:
user: login: LunaChevalier
body: 不要な対応が入っていたり、もともと話していた内容と異なる対応が入っていたりしているので、全体的に見直しをお願いします
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T04:52:28Z
user: login: LunaChevalier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T06:30:07Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-18T02:51:39Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-18T02:55:05Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-18T03:59:43Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-18T04:01:31Z
user: login: LunaChevalier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T04:33:36Z
user: login: LunaChevalier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T04:34:17Z
user: login: LunaChevalier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T10:41:11Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-19T06:16:47Z
user: login: LunaChevalier
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T06:30:28Z
user: login: kamiya777
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-19T08:29:42Z
user: login: LunaChevalier
body: ![LGTM](https://image.lgtmoon.dev/384286)
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-24T08:39:36Z

Review comments:
diff_hunk: @@ -6,47 +6,40 @@
 //
 
 import SwiftUI
-
+import Combine
 
 class ContentViewModel: ObservableObject {
     @Published var accessToken: String = ""
+    @Published var isLoggedIn: Bool = false
+    @Published var errorMessage: String?
+    @Published var showAlert: Bool = false
     
-    private let apiBaseURL = "https://qiita.com/api/v2/authenticated_user"
+    private var cancellables = Set<AnyCancellable>()
     
     // ログインアクション
-    func loginAction(completion: @escaping (Result<Void, Error>) -> Void) {
-        guard !accessToken.isEmpty else {
-            completion(.failure(NSError(domain: "Invalid Input", code: 400, userInfo: [NSLocalizedDescriptionKey: "アクセストークンが入力されていません!"])))
-            return
-        }
-        
-        var request = URLRequest(url: URL(string: apiBaseURL)!)
path: QiitaApp/ViewModels/ContentViewModel.swift
user: login: LunaChevalier
body: ![ask](https://img.shields.io/badge/review-ask-blue.svg)
このあたりはローカライズ化に関連がなさそうな気がするのですが、いかがでしょうか?
created_at: 2025-03-14T04:09:36Z
author_association: COLLABORATOR
diff_hunk: @@ -6,47 +6,40 @@
 //
 
 import SwiftUI
-
+import Combine
 
 class ContentViewModel: ObservableObject {
     @Published var accessToken: String = ""
+    @Published var isLoggedIn: Bool = false
+    @Published var errorMessage: String?
+    @Published var showAlert: Bool = false
     
-    private let apiBaseURL = "https://qiita.com/api/v2/authenticated_user"
+    private var cancellables = Set<AnyCancellable>()
     
     // ログインアクション
-    func loginAction(completion: @escaping (Result<Void, Error>) -> Void) {
-        guard !accessToken.isEmpty else {
-            completion(.failure(NSError(domain: "Invalid Input", code: 400, userInfo: [NSLocalizedDescriptionKey: "アクセストークンが入力されていません!"])))
-            return
-        }
-        
-        var request = URLRequest(url: URL(string: apiBaseURL)!)
-        request.httpMethod = "GET"
-        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
-        
-        let task = URLSession.shared.dataTask(with: request) { data, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    completion(.failure(error))
-                }
+    func loginAction() {
+            guard !accessToken.isEmpty else {
+                errorMessage = "アクセストークンが入力されていません!"
+                showAlert = true
                 return
             }
-            
-            if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
-                // 認証成功
-                DispatchQueue.main.async {
-                    completion(.success(()))
-                }
-            } else {
-                // 認証失敗
-                let error = NSError(domain: "Authentication Failed", code: 401, userInfo: [NSLocalizedDescriptionKey: "アクセストークンが間違っています。"])
-                DispatchQueue.main.async {
-                    completion(.failure(error))
-                }
-            }
+
+            ApiService.shared.fetchUserData(accessToken: accessToken)
path: QiitaApp/ViewModels/ContentViewModel.swift
user: login: LunaChevalier
body: ![ask](https://img.shields.io/badge/review-ask-blue.svg)
ローカライズ化に関連がなさそうな気がするのですが、いかがでしょうか?


created_at: 2025-03-14T04:10:24Z
author_association: COLLABORATOR
path: QiitaApp/Services/ApiService.swift
user: login: LunaChevalier
body: ![ask](https://img.shields.io/badge/review-ask-blue.svg)
ローカライズ化に関連がないと思います
created_at: 2025-03-14T04:10:57Z
author_association: COLLABORATOR
diff_hunk: @@ -12,47 +12,26 @@ class ItemDetailViewModel: ObservableObject {
     @Published var item: Item?
     @Published var isLoading: Bool = false
     @Published var errorMessage: String?
-    
+
+    private var cancellables = Set<AnyCancellable>()
+
+    // アイテム詳細を取得
     func fetchItemDetails(itemId: String) {
         isLoading = true
-        let urlString = "https://qiita.com/api/v2/items/\(itemId)"
-        
-        guard let url = URL(string: urlString) else {
-            errorMessage = "Invalid URL"
-            isLoading = false
-            return
-        }
         
-        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    self?.errorMessage = error.localizedDescription
-                }
-                return
-            }
-            
-            guard let data = data else {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "No data received"
-                }
-                return
-            }
-            
-            do {
-                // JSONをデコードしてItemを作成
-                let decodedApiItem = try JSONDecoder().decode(ApiItem.self, from: data)
-                
-                // UI関連の処理だけをメインスレッドで行う
-                DispatchQueue.main.async {
-                    self?.isLoading = false
-                    self?.item = Item(from: decodedApiItem)
-                }
-            } catch {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "Failed to decode data"
+        ApiService.shared.fetchItemDetails(itemId: itemId)
+            .receive(on: DispatchQueue.main)
+            .sink(receiveCompletion: { [weak self] completion in
+                switch completion {
+                case .failure(let error):
+                    self?.errorMessage = "エラーが発生しました: \(error.localizedDescription)"
path: QiitaApp/ViewModels/ItemDetailViewModel.swift
user: login: LunaChevalier
body: ![should](https://img.shields.io/badge/review-should-orange.svg)
`エラーが発生しました:`のローカライズが漏れています
created_at: 2025-03-14T04:12:03Z
author_association: COLLABORATOR
diff_hunk: @@ -36,26 +36,26 @@ struct MyPageView: View {
                             viewModel.fetchUserData(accessToken: token)
                         }
                     }) {
-                        Text("再読み込み")
+                        Text(LocalizedStringKey("Reload"))
                             .foregroundColor(.blue)
                             .padding()
                             .background(Capsule().strokeBorder(Color.blue, lineWidth: 2))
                     }
                 } else if let user = viewModel.user {
                     // 正常にユーザー情報が取得できた場合
                     VStack(alignment: .leading, spacing: 10) {
-                        Text("ユーザー名: \(user.name)")
+                        Text(String(format: NSLocalizedString("User name: %@", comment: ""), arguments: [user.name]))
                             .font(.headline)
                         Spacer().frame(height: 20)
-                        Text("フォローしている数: \(user.followeesCount)")
-                        Text("フォローされている数: \(user.followersCount)")
-                        Text("Qiitaに公開している記事の数: \(user.itemsCount)")
-                        Text("移住地: \(user.location)")
+                        Text(String(format: NSLocalizedString("Number of followers: %d", comment: ""), arguments: [user.followeesCount]))
path: QiitaApp/Views/MyPageView.swift
user: login: LunaChevalier
body: ![ask](https://img.shields.io/badge/review-ask-blue.svg)
`LocalizedStringKey`を使う想定だったと思うのですが、`String`になっています
created_at: 2025-03-14T04:28:48Z
author_association: COLLABORATOR
path: QiitaApp/en.lproj/Localizable.strings
user: login: LunaChevalier
body: ![should](https://img.shields.io/badge/review-should-orange.svg)
日本語だけローカライズできればOKなので、英語のstringsファイルは不要です
created_at: 2025-03-14T04:29:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: ![should](https://img.shields.io/badge/review-should-orange.svg)
スペースで単語を区切ったりキャメルケースで命名されたりと命名規則がバラバラです
調べた限り、キー名はキャメルケースで命名することが多そうなので、キャメルケースでお願いします
created_at: 2025-03-14T04:31:31Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
+"Login" = "ログイン";
+"Use without login" = "ログインせずに利用する";
+"Error" = "エラー";
+"OK" = "OK";
+"Unknown error" = "不明なエラー";
+"User" = "ユーザー名: -";
+"Number of followers" = "フォローしている数: -";
+"Number of followees" = "フォローされている数: -";
+"Number of Qiita articles" = "Qiitaに公開している記事の数: -";
+"Location" = "移住地: -";
+"Reload" = "再読み込み";
+"User name: %@" = "ユーザー名: %@";
+"Number of followers: %d" = "フォローしている数: %d";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: ![ask](https://img.shields.io/badge/review-ask-blue.svg)
キー名に`%d`が含まれていますが、なぜありますか?
created_at: 2025-03-14T04:51:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: ちなみに、スペースで単語を区切ったのは何か理由があったりしますか?
created_at: 2025-03-14T06:30:07Z
author_association: COLLABORATOR
diff_hunk: @@ -12,47 +12,26 @@ class ItemDetailViewModel: ObservableObject {
     @Published var item: Item?
     @Published var isLoading: Bool = false
     @Published var errorMessage: String?
-    
+
+    private var cancellables = Set<AnyCancellable>()
+
+    // アイテム詳細を取得
     func fetchItemDetails(itemId: String) {
         isLoading = true
-        let urlString = "https://qiita.com/api/v2/items/\(itemId)"
-        
-        guard let url = URL(string: urlString) else {
-            errorMessage = "Invalid URL"
-            isLoading = false
-            return
-        }
         
-        let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
-            if let error = error {
-                DispatchQueue.main.async {
-                    self?.errorMessage = error.localizedDescription
-                }
-                return
-            }
-            
-            guard let data = data else {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "No data received"
-                }
-                return
-            }
-            
-            do {
-                // JSONをデコードしてItemを作成
-                let decodedApiItem = try JSONDecoder().decode(ApiItem.self, from: data)
-                
-                // UI関連の処理だけをメインスレッドで行う
-                DispatchQueue.main.async {
-                    self?.isLoading = false
-                    self?.item = Item(from: decodedApiItem)
-                }
-            } catch {
-                DispatchQueue.main.async {
-                    self?.errorMessage = "Failed to decode data"
+        ApiService.shared.fetchItemDetails(itemId: itemId)
+            .receive(on: DispatchQueue.main)
+            .sink(receiveCompletion: { [weak self] completion in
+                switch completion {
+                case .failure(let error):
+                    self?.errorMessage = "エラーが発生しました: \(error.localizedDescription)"
path: QiitaApp/ViewModels/ItemDetailViewModel.swift
user: login: kamiya777
body: ローカライズ対応します。
created_at: 2025-03-18T02:51:38Z
author_association: OWNER
diff_hunk: @@ -36,26 +36,26 @@ struct MyPageView: View {
                             viewModel.fetchUserData(accessToken: token)
                         }
                     }) {
-                        Text("再読み込み")
+                        Text(LocalizedStringKey("Reload"))
                             .foregroundColor(.blue)
                             .padding()
                             .background(Capsule().strokeBorder(Color.blue, lineWidth: 2))
                     }
                 } else if let user = viewModel.user {
                     // 正常にユーザー情報が取得できた場合
                     VStack(alignment: .leading, spacing: 10) {
-                        Text("ユーザー名: \(user.name)")
+                        Text(String(format: NSLocalizedString("User name: %@", comment: ""), arguments: [user.name]))
                             .font(.headline)
                         Spacer().frame(height: 20)
-                        Text("フォローしている数: \(user.followeesCount)")
-                        Text("フォローされている数: \(user.followersCount)")
-                        Text("Qiitaに公開している記事の数: \(user.itemsCount)")
-                        Text("移住地: \(user.location)")
+                        Text(String(format: NSLocalizedString("Number of followers: %d", comment: ""), arguments: [user.followeesCount]))
path: QiitaApp/Views/MyPageView.swift
user: login: kamiya777
body: `LocalizedStringKey`に修正します。
created_at: 2025-03-18T02:55:05Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
+"Login" = "ログイン";
+"Use without login" = "ログインせずに利用する";
+"Error" = "エラー";
+"OK" = "OK";
+"Unknown error" = "不明なエラー";
+"User" = "ユーザー名: -";
+"Number of followers" = "フォローしている数: -";
+"Number of followees" = "フォローされている数: -";
+"Number of Qiita articles" = "Qiitaに公開している記事の数: -";
+"Location" = "移住地: -";
+"Reload" = "再読み込み";
+"User name: %@" = "ユーザー名: %@";
+"Number of followers: %d" = "フォローしている数: %d";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: kamiya777
body: こちらはvalueの値をそのままキーに変換したのでその時に`%d`が入ってしまっていたので
削除します。
created_at: 2025-03-18T03:59:43Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: kamiya777
body: キャメルケースで修正します。

> ちなみに、スペースで単語を区切ったのは何か理由があったりしますか?

valueの値をサイトでそのまま変換しただけだったので特に理由はありませんので
修正します。
created_at: 2025-03-18T04:01:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: > valueの値をサイトでそのまま変換しただけ

命名としてふさわしくないと思います
例えば「ユーザー名」と表示している箇所が「ニックネーム」に変更する場合、この命名の仕方だと毎回キー名を変更することになります
どの画面のどの画面パーツに使うのか?を軸に命名した方が良いです
created_at: 2025-03-18T04:33:36Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
+"Login" = "ログイン";
+"Use without login" = "ログインせずに利用する";
+"Error" = "エラー";
+"OK" = "OK";
+"Unknown error" = "不明なエラー";
+"User" = "ユーザー名: -";
+"Number of followers" = "フォローしている数: -";
+"Number of followees" = "フォローされている数: -";
+"Number of Qiita articles" = "Qiitaに公開している記事の数: -";
+"Location" = "移住地: -";
+"Reload" = "再読み込み";
+"User name: %@" = "ユーザー名: %@";
+"Number of followers: %d" = "フォローしている数: %d";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: こちらも命名の仕方としてふさわしくないので、命名の仕方から修正をお願いします
created_at: 2025-03-18T04:34:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: @kamiya777 命名に関する対応が漏れているので、修正お願いします!
created_at: 2025-03-18T10:41:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: kamiya777
body: @LunaChevalier 

> 命名としてふさわしくないと思います
> 例えば「ユーザー名」と表示している箇所が「ニックネーム」に変更する場合、この命名の仕方だと毎回キー名を変更することになります
> どの画面のどの画面パーツに使うのか?を軸に命名した方が良いです

すみません、ここがあまりよく分からないのです。
例えば`アクセストークン`だと`loginScreenAccessToken`みたいな感じでしょうか？

created_at: 2025-03-19T06:16:47Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: LunaChevalier
body: > 例えばアクセストークンだとloginScreenAccessTokenみたいな感じでしょうか？

文字列ベースではなく、画面パーツで命名した方が良いかなと思います
例に挙がっている「アクセストークン」を表示する箇所は、ログイン画面のアクセストークンを入力する箇所のタイトル部分になるので、`loginScreenAccessTokenTitle`のような命名をするイメージです
created_at: 2025-03-19T06:30:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,36 @@
+/* 
+  Localizable.strings
+  QiitaApp
+
+  Created by kamiya on 2025/03/13.
+  
+*/
+
+"Access Token" = "アクセストークン";
path: QiitaApp/ja.lproj/Localizable.strings
user: login: kamiya777
body: @LunaChevalier 
ログインボタンだと`loginScreenLoginButton`になると思うのですが
これはこれで正しいのでしょうか？
created_at: 2025-03-19T08:29:42Z
author_association: OWNER

PR comments:
user: login: kamiya777
created_at: 2025-03-17T08:30:00Z
body: ローカライズ化以外の箇所は恐らくmasterブランチをマージした時に
発生したものかと思われますので、一度調べます。
以下のファイルがローカライズ対応した対象ファイルです。
```
QiitaApp/Views/ContentView.swift
QiitaApp/Views/ItemDetailView.swift
QiitaApp/Views/MainTabView.swift
QiitaApp/Views/MyPageView.swift
QiitaApp/Views/SearchResultView.swift
QiitaApp/Views/SearchView.swift
QiitaApp/en.lproj/Localizable.strings
QiitaApp/ja.lproj/Localizable.strings
```
author_association: OWNER
user: login: kamiya777
created_at: 2025-03-18T10:09:52Z
body: @LunaChevalier 
レビュー後の修正を行いましたのでご確認よろしくお願いします。
author_association: OWNER
user: login: kamiya777
created_at: 2025-03-21T02:10:33Z
body: @LunaChevalier 
命名に関する対応を行いましたのでご確認よろしくお願いします。
author_association: OWNER
user: login: kamiya777
created_at: 2025-03-21T09:16:03Z
body: @LunaChevalier 
英語のstringsファイル削除を行いましたのでご確認よろしくお願いします。
author_association: OWNER