Bladeren bron

feat: implement Google OAuth, localize fonts, and fix translations

unknown 2 dagen geleden
bovenliggende
commit
b62710efac

+ 1 - 0
.env.production

@@ -1,2 +1,3 @@
 VITE_API_URL=https://radionica3d.me/api
 VITE_WS_URL=wss://radionica3d.me/ws
+VITE_GOOGLE_CLIENT_ID=254513580225-j893ad8nd2f2celd2thn1l42miqm9e7s.apps.googleusercontent.com

+ 1 - 0
backend/config.py

@@ -45,3 +45,4 @@ EFI_ENU_CODE = os.getenv("EFI_ENU_CODE", "xx123yy456")
 EFI_BUS_UNIT = os.getenv("EFI_BUS_UNIT", "br123")      
 EFI_OPERATOR = os.getenv("EFI_OPERATOR", "op123")      
 EFI_STAGING = os.getenv("EFI_STAGING", "True").lower() == "true"
+GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID", "")

+ 45 - 4
backend/routers/auth.py

@@ -10,6 +10,14 @@ import uuid
 from datetime import datetime, timedelta
 import locales
 from dependencies import get_current_user, require_admin
+import config
+
+try:
+    from google.oauth2 import id_token
+    from google.auth.transport import requests as google_requests
+except ImportError:
+    id_token = None
+    google_requests = None
 
 router = APIRouter(prefix="/auth", tags=["auth"])
 
@@ -79,10 +87,38 @@ async def login(request: Request, user_data: schemas.UserLogin, lang: str = "en"
 
 @router.post("/social-login", response_model=schemas.Token)
 async def social_login(request: Request, data: schemas.SocialLogin):
-    user = db.execute_query("SELECT id, email, role, is_active FROM users WHERE email = %s", (data.email,))
+    email = data.email.lower()
+    first_name = data.first_name
+    last_name = data.last_name
+
+    # 1. Verify token if provider is Google
+    if data.provider == 'google':
+        if not id_token or not config.GOOGLE_CLIENT_ID:
+            raise HTTPException(status_code=500, detail="Google Auth not configured on server")
+        
+        try:
+            # Verify the ID token
+            idinfo = id_token.verify_oauth2_token(data.token, google_requests.Request(), config.GOOGLE_CLIENT_ID)
+            
+            # ID token is valid. Get user's Google info
+            if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
+                raise ValueError('Wrong issuer.')
+            
+            email = idinfo['email'].lower()
+            first_name = idinfo.get('given_name', first_name)
+            last_name = idinfo.get('family_name', last_name)
+            
+        except Exception as e:
+            print(f"Google Token Verification Error: {e}")
+            raise HTTPException(status_code=401, detail="Invalid Google token")
+
+    # 2. Proceed with login/registration
+    user = db.execute_query("SELECT id, email, role, is_active FROM users WHERE email = %s", (email,))
+    
     if user:
         if not user[0].get('is_active', True):
             raise HTTPException(status_code=403, detail="Your account has been suspended.")
+        
         access_token = auth_utils.create_access_token(
             data={"sub": user[0]['email'], "id": user[0]['id'], "role": user[0]['role']}
         )
@@ -90,10 +126,15 @@ async def social_login(request: Request, data: schemas.SocialLogin):
     else:
         ip_address = request.client.host if request.client else None
         hashed_password = auth_utils.get_password_hash(str(uuid.uuid4()))
-        query = "INSERT INTO users (email, password_hash, first_name, last_name, preferred_language, role, ip_address) VALUES (%s, %s, %s, %s, %s, %s, %s)"
-        params = (data.email, hashed_password, data.first_name, data.last_name, data.preferred_language, 'user', ip_address)
+        
+        query = """
+        INSERT INTO users (email, password_hash, first_name, last_name, preferred_language, role, ip_address) 
+        VALUES (%s, %s, %s, %s, %s, %s, %s)
+        """
+        params = (email, hashed_password, first_name, last_name, data.preferred_language, 'user', ip_address)
         user_id = db.execute_commit(query, params)
-        access_token = auth_utils.create_access_token(data={"sub": data.email, "id": user_id, "role": 'user'})
+        
+        access_token = auth_utils.create_access_token(data={"sub": email, "id": user_id, "role": 'user'})
         return {"access_token": access_token, "token_type": "bearer"}
 
 @router.post("/logout")

+ 1 - 4
index.html

@@ -5,10 +5,7 @@
     <link rel="icon" type="image/png" href="/favicon.png" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700;800&display=swap">
-    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
+    <link rel="stylesheet" href="/fonts/fonts.css">
     
     <title>Radionica 3D | Professional 3D Printing in Montenegro</title>
     <link rel="canonical" href="https://radionica3d.me/" />

+ 8 - 1
nginx.conf

@@ -37,8 +37,15 @@ server {
         access_log off;
     }
 
+    # Font files (local hosting, long-term cache)
+    location /fonts/ {
+        expires 10y;
+        add_header Cache-Control "public, immutable";
+        access_log off;
+    }
+
     # Other static files
-    location ~* \.(?:ico|gif|jpe?g|png|woff2?|eot|otf|ttf|svg|webp|avif)$ {
+    location ~* \.(?:ico|gif|jpe?g|png|svg|webp|avif)$ {
         expires 7d;
         add_header Cache-Control "public";
         access_log off;

BIN
public/fonts/QGYvz_MVcBeNP4NJtEtq.woff2


BIN
public/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2


BIN
public/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2


+ 468 - 0
public/fonts/fonts.css

@@ -0,0 +1,468 @@
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 300;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2JL7SUc.woff2) format('woff2');
+  unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa0ZL7SUc.woff2) format('woff2');
+  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2ZL7SUc.woff2) format('woff2');
+  unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1pL7SUc.woff2) format('woff2');
+  unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* vietnamese */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa2pL7SUc.woff2) format('woff2');
+  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa25L7SUc.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Inter';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 400;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJtEtq.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 500;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJtEtq.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 600;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJtEtq.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 700;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJtEtq.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* latin-ext */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJuktqQ4E.woff2) format('woff2');
+  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+  font-family: 'Outfit';
+  font-style: normal;
+  font-weight: 800;
+  font-display: swap;
+  src: url(/fonts/QGYvz_MVcBeNP4NJtEtq.woff2) format('woff2');
+  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}

+ 64 - 0
scratch/find_leaks.py

@@ -0,0 +1,64 @@
+import json
+import sys
+from pathlib import Path
+
+# Ensure stdout handles UTF-8 correctly for console printing
+if hasattr(sys.stdout, 'reconfigure'):
+    sys.stdout.reconfigure(encoding='utf-8')
+
+LOCALES_DIR = Path("src/locales")
+USER_MASTER = LOCALES_DIR / "translations.user.json"
+ADMIN_MASTER = LOCALES_DIR / "translations.admin.json"
+LANGUAGES = ["en", "me", "ru", "ua"]
+
+def is_cyrillic(s):
+    """Checks if a string contains any Cyrillic characters."""
+    return any('\u0400' <= c <= '\u04FF' for c in s)
+
+def find_leaks(data, path=""):
+    """
+    Recursively searches through translation objects to find:
+    1. Cyrillic characters in 'en' or 'me' fields (leaked Russian/Ukrainian).
+    2. Specific Russian-only characters (ы, ъ, ё, э) in 'ua' fields.
+    """
+    leaks = []
+    if isinstance(data, dict):
+        # If this is a leaf leaf node containing language keys
+        if all(lang in data for lang in LANGUAGES):
+            for lang in LANGUAGES:
+                val = str(data[lang])
+                
+                # Check for Cyrillic in languages that use Latin (en, me)
+                if lang in ["en", "me"] and is_cyrillic(val):
+                    leaks.append((f"{path} [{lang}]", val))
+                
+                # Check for Russian-only characters in Ukrainian
+                if lang == "ua":
+                    russian_only = ['ы', 'ъ', 'ё', 'э', 'Ы', 'Ъ', 'Ё', 'Э']
+                    if any(c in val for c in russian_only):
+                        leaks.append((f"{path} [ua]", val))
+        else:
+            # Continue deeper into the structure
+            for k, v in data.items():
+                leaks.extend(find_leaks(v, f"{path}.{k}" if path else k))
+    return leaks
+
+def run_check(master_path, name):
+    print(f"\n--- Checking for leaks in {name} ({master_path.name}) ---")
+    if not master_path.exists():
+        print(f"Error: {master_path} not found.")
+        return
+
+    with open(master_path, "r", encoding="utf-8") as f:
+        data = json.load(f)
+
+    leaks = find_leaks(data)
+    if not leaks:
+        print("Success: No language leaks found!")
+    else:
+        for p, val in leaks:
+            print(f"Leak found at {p}: {val}")
+
+if __name__ == "__main__":
+    run_check(USER_MASTER, "User Translations")
+    run_check(ADMIN_MASTER, "Admin Translations")

+ 1 - 1
server_update.sh

@@ -22,7 +22,7 @@ echo "updating backend..."
 echo "restarting services..."
 sudo systemctl restart radionica-backend
 cp /var/www/radionica3d/nginx.conf /etc/nginx/sites-available/radionica3d
-sudo systemctl restart nginx
+sudo systemctl reload nginx
 sudo chown -R www-data:www-data $PROJECT_DIR
 
 echo "==== DEPLOY FINISHED: $(date) ===="

+ 28 - 28
src/locales/me.json

@@ -97,7 +97,7 @@
   "blog": {
     "back": "Nazad na Blog",
     "categories": {
-      "caseStudies": "Кейсы",
+      "caseStudies": "Studije slučaja",
       "industry": "Industrija",
       "materials": "Materijali",
       "technology": "Tehnologija",
@@ -298,45 +298,45 @@
         "title": "Formati"
       },
       "requirements": {
-        "normals": "Correct Normals",
-        "scale": "Proper Scale",
+        "normals": "Ispravne normale",
+        "scale": "Ispravna razmjera",
         "title": "Zahtjevi",
-        "wallThickness": "Min Wall Thickness",
-        "watertight": "Watertight"
+        "wallThickness": "Min debljina zida",
+        "watertight": "Zatvorena geometrija (Watertight)"
       },
       "title": "Priprema fajla"
     },
     "help": {
-      "contact": "Contact",
-      "description": "Ask us.",
-      "helpCenter": "Help Center",
-      "title": "Need Help?"
+      "contact": "Kontaktirajte nas",
+      "description": "Pitajte nas bilo šta o 3D štampi.",
+      "helpCenter": "Centar za pomoć",
+      "title": "Potrebna pomoć?"
     },
     "materialSelection": {
       "abs": {
-        "bestFor": "Tech parts",
-        "considerations": "Heat resistant",
+        "bestFor": "Tehnički djelovi",
+        "considerations": "Otporan na toplotu",
         "name": "ABS"
       },
       "petg": {
-        "bestFor": "Durable",
-        "considerations": "Easy print",
+        "bestFor": "Izdržljivi djelovi",
+        "considerations": "Lako se štampa",
         "name": "PETG"
       },
       "pla": {
-        "bestFor": "Prototypes",
-        "considerations": "Bio-degradable",
+        "bestFor": "Prototipovi",
+        "considerations": "Biorazgradiv",
         "name": "PLA"
       },
       "resin": {
-        "bestFor": "Detail",
-        "considerations": "UV sensitive",
+        "bestFor": "Detalji",
+        "considerations": "UV osjetljiv",
         "name": "Resin"
       },
       "table": {
-        "bestFor": "Best For",
-        "considerations": "Notes",
-        "material": "Material"
+        "bestFor": "Najbolje za",
+        "considerations": "Napomene",
+        "material": "Materijal"
       },
       "title": "Materijali"
     },
@@ -393,20 +393,20 @@
     },
     "orderingProcess": {
       "step1": {
-        "description": "Send file",
+        "description": "Pošaljite model",
         "title": "Upload"
       },
       "step2": {
-        "description": "We craft",
-        "title": "Print"
+        "description": "Mi izrađujemo",
+        "title": "Štampa"
       },
       "step3": {
-        "description": "By mail",
-        "title": "Ship"
+        "description": "Dostava kurirom",
+        "title": "Dostava"
       },
       "step4": {
-        "description": "Trust model",
-        "title": "Pay"
+        "description": "Sigurno plaćanje",
+        "title": "Plaćanje"
       },
       "title": "Process"
     },
@@ -504,7 +504,7 @@
     "aiDisclaimer": "Sve fotografije objašnjenja su generisane pomoću vještačke inteligencije u ilustrativne svrhe.",
     "description": "Proizvodi napravljeni FDM metodom (slojevito topljenje plastike) imaju niz vizuelnih i taktilnih karakteristika koje su normalne za tehnologiju i ne smatramo ih defektima.",
     "disclaimer": {
-      "text": "Ove karakteristike su posledica same prirode tehnologije FDM štampe.",
+      "text": "Ove karakteristike su posledica same prirode tehnologije FDM štampe i ne utiču na funkcionalnost proizvoda.",
       "title": "Garancija kvaliteta"
     },
     "items": {

+ 28 - 28
src/locales/ru.json

@@ -298,45 +298,45 @@
         "title": "Форматы"
       },
       "requirements": {
-        "normals": "Correct Normals",
-        "scale": "Proper Scale",
+        "normals": "Правильные нормали",
+        "scale": "Правильный масштаб",
         "title": "Требования",
-        "wallThickness": "Min Wall Thickness",
-        "watertight": "Watertight"
+        "wallThickness": "Мин. толщина стенки",
+        "watertight": "Герметичность (Watertight)"
       },
       "title": "Подготовка файлов"
     },
     "help": {
-      "contact": "Contact",
-      "description": "Ask us.",
-      "helpCenter": "Help Center",
-      "title": "Need Help?"
+      "contact": "Связаться",
+      "description": "Задайте нам любой вопрос о 3D-печати.",
+      "helpCenter": "Центр помощи",
+      "title": "Нужна помощь?"
     },
     "materialSelection": {
       "abs": {
-        "bestFor": "Tech parts",
-        "considerations": "Heat resistant",
+        "bestFor": "Технические детали",
+        "considerations": "Термостойкий",
         "name": "ABS"
       },
       "petg": {
-        "bestFor": "Durable",
-        "considerations": "Easy print",
+        "bestFor": "Прочные изделия",
+        "considerations": "Легкая печать",
         "name": "PETG"
       },
       "pla": {
-        "bestFor": "Prototypes",
-        "considerations": "Bio-degradable",
+        "bestFor": "Прототипы",
+        "considerations": "Биоразлагаемый",
         "name": "PLA"
       },
       "resin": {
-        "bestFor": "Detail",
-        "considerations": "UV sensitive",
+        "bestFor": "Мелкие детали",
+        "considerations": "УФ-чувствительный",
         "name": "Resin"
       },
       "table": {
-        "bestFor": "Best For",
-        "considerations": "Notes",
-        "material": "Material"
+        "bestFor": "Подходит для",
+        "considerations": "Особенности",
+        "material": "Материал"
       },
       "title": "Материалы"
     },
@@ -393,20 +393,20 @@
     },
     "orderingProcess": {
       "step1": {
-        "description": "Send file",
-        "title": "Upload"
+        "description": "Загрузите файл",
+        "title": "Загрузка"
       },
       "step2": {
-        "description": "We craft",
-        "title": "Print"
+        "description": "Мы изготавливаем",
+        "title": "Печать"
       },
       "step3": {
-        "description": "By mail",
-        "title": "Ship"
+        "description": "Доставка почтой",
+        "title": "Доставка"
       },
       "step4": {
-        "description": "Trust model",
-        "title": "Pay"
+        "description": "Удобная оплата",
+        "title": "Оплата"
       },
       "title": "Process"
     },
@@ -504,7 +504,7 @@
     "aiDisclaimer": "Все поясняющие фотографии сгенерированы ИИ для наглядности.",
     "description": "Изделия, изготовленные методом FDM (послойного наплавления пластика), имеют ряд визуальных и тактильных особенностей, которые являются нормой технологии и не считаются дефектами.",
     "disclaimer": {
-      "text": "Эти особенности обусловлены самой природой технологии FDM-печати.",
+      "text": "Эти особенности обусловлены самой природой технологии FDM-печати и не влияют на функциональность изделия.",
       "title": "Гарантия качества"
     },
     "items": {

+ 86 - 86
src/locales/translations.user.json

@@ -389,7 +389,7 @@
     "categories": {
       "caseStudies": {
         "en": "Case Studies",
-        "me": "Кейсы",
+        "me": "Studije slučaja",
         "ru": "Кейсы",
         "ua": "Кейси"
       },
@@ -1230,15 +1230,15 @@
       "requirements": {
         "normals": {
           "en": "Correct Normals",
-          "me": "Correct Normals",
-          "ru": "Correct Normals",
-          "ua": "Correct Normals"
+          "me": "Ispravne normale",
+          "ru": "Правильные нормали",
+          "ua": "Правильні нормалі"
         },
         "scale": {
           "en": "Proper Scale",
-          "me": "Proper Scale",
-          "ru": "Proper Scale",
-          "ua": "Proper Scale"
+          "me": "Ispravna razmjera",
+          "ru": "Правильный масштаб",
+          "ua": "Правильний масштаб"
         },
         "title": {
           "en": "Requirements",
@@ -1248,15 +1248,15 @@
         },
         "wallThickness": {
           "en": "Min Wall Thickness",
-          "me": "Min Wall Thickness",
-          "ru": "Min Wall Thickness",
-          "ua": "Min Wall Thickness"
+          "me": "Min debljina zida",
+          "ru": "Мин. толщина стенки",
+          "ua": "Мін. товщина стінки"
         },
         "watertight": {
           "en": "Watertight",
-          "me": "Watertight",
-          "ru": "Watertight",
-          "ua": "Watertight"
+          "me": "Zatvorena geometrija (Watertight)",
+          "ru": "Герметичность (Watertight)",
+          "ua": "Герметичність (Watertight)"
         }
       },
       "title": {
@@ -1269,42 +1269,42 @@
     "help": {
       "contact": {
         "en": "Contact",
-        "me": "Contact",
-        "ru": "Contact",
-        "ua": "Contact"
+        "me": "Kontaktirajte nas",
+        "ru": "Связаться",
+        "ua": "Зв'язатися"
       },
       "description": {
         "en": "Ask us.",
-        "me": "Ask us.",
-        "ru": "Ask us.",
-        "ua": "Ask us."
+        "me": "Pitajte nas bilo šta o 3D štampi.",
+        "ru": "Задайте нам любой вопрос о 3D-печати.",
+        "ua": "Задайте нам будь-яке питання про 3D-друк."
       },
       "helpCenter": {
         "en": "Help Center",
-        "me": "Help Center",
-        "ru": "Help Center",
-        "ua": "Help Center"
+        "me": "Centar za pomoć",
+        "ru": "Центр помощи",
+        "ua": "Центр допомоги"
       },
       "title": {
         "en": "Need Help?",
-        "me": "Need Help?",
-        "ru": "Need Help?",
-        "ua": "Need Help?"
+        "me": "Potrebna pomoć?",
+        "ru": "Нужна помощь?",
+        "ua": "Потрібна допомога?"
       }
     },
     "materialSelection": {
       "abs": {
         "bestFor": {
           "en": "Tech parts",
-          "me": "Tech parts",
-          "ru": "Tech parts",
-          "ua": "Tech parts"
+          "me": "Tehnički djelovi",
+          "ru": "Технические детали",
+          "ua": "Технічні деталі"
         },
         "considerations": {
           "en": "Heat resistant",
-          "me": "Heat resistant",
-          "ru": "Heat resistant",
-          "ua": "Heat resistant"
+          "me": "Otporan na toplotu",
+          "ru": "Термостойкий",
+          "ua": "Термостійкий"
         },
         "name": {
           "en": "ABS",
@@ -1316,15 +1316,15 @@
       "petg": {
         "bestFor": {
           "en": "Durable",
-          "me": "Durable",
-          "ru": "Durable",
-          "ua": "Durable"
+          "me": "Izdržljivi djelovi",
+          "ru": "Прочные изделия",
+          "ua": "Міцні вироби"
         },
         "considerations": {
           "en": "Easy print",
-          "me": "Easy print",
-          "ru": "Easy print",
-          "ua": "Easy print"
+          "me": "Lako se štampa",
+          "ru": "Легкая печать",
+          "ua": "Легкий друк"
         },
         "name": {
           "en": "PETG",
@@ -1336,15 +1336,15 @@
       "pla": {
         "bestFor": {
           "en": "Prototypes",
-          "me": "Prototypes",
-          "ru": "Prototypes",
-          "ua": "Prototypes"
+          "me": "Prototipovi",
+          "ru": "Прототипы",
+          "ua": "Прототипи"
         },
         "considerations": {
           "en": "Bio-degradable",
-          "me": "Bio-degradable",
-          "ru": "Bio-degradable",
-          "ua": "Bio-degradable"
+          "me": "Biorazgradiv",
+          "ru": "Биоразлагаемый",
+          "ua": "Біорозкладний"
         },
         "name": {
           "en": "PLA",
@@ -1356,15 +1356,15 @@
       "resin": {
         "bestFor": {
           "en": "Detail",
-          "me": "Detail",
-          "ru": "Detail",
-          "ua": "Detail"
+          "me": "Detalji",
+          "ru": "Мелкие детали",
+          "ua": "Дрібні деталі"
         },
         "considerations": {
           "en": "UV sensitive",
-          "me": "UV sensitive",
-          "ru": "UV sensitive",
-          "ua": "UV sensitive"
+          "me": "UV osjetljiv",
+          "ru": "УФ-чувствительный",
+          "ua": "УФ-чутливий"
         },
         "name": {
           "en": "Resin",
@@ -1376,21 +1376,21 @@
       "table": {
         "bestFor": {
           "en": "Best For",
-          "me": "Best For",
-          "ru": "Best For",
-          "ua": "Best For"
+          "me": "Najbolje za",
+          "ru": "Подходит для",
+          "ua": "Підходить для"
         },
         "considerations": {
           "en": "Notes",
-          "me": "Notes",
-          "ru": "Notes",
-          "ua": "Notes"
+          "me": "Napomene",
+          "ru": "Особенности",
+          "ua": "Особливості"
         },
         "material": {
           "en": "Material",
-          "me": "Material",
-          "ru": "Material",
-          "ua": "Material"
+          "me": "Materijal",
+          "ru": "Материал",
+          "ua": "Матеріал"
         }
       },
       "title": {
@@ -1600,57 +1600,57 @@
       "step1": {
         "description": {
           "en": "Send file",
-          "me": "Send file",
-          "ru": "Send file",
-          "ua": "Send file"
+          "me": "Pošaljite model",
+          "ru": "Загрузите файл",
+          "ua": "Завантажте файл"
         },
         "title": {
           "en": "Upload",
           "me": "Upload",
-          "ru": "Upload",
-          "ua": "Upload"
+          "ru": "Загрузка",
+          "ua": "Завантаження"
         }
       },
       "step2": {
         "description": {
           "en": "We craft",
-          "me": "We craft",
-          "ru": "We craft",
-          "ua": "We craft"
+          "me": "Mi izrađujemo",
+          "ru": "Мы изготавливаем",
+          "ua": "Ми виготовляємо"
         },
         "title": {
           "en": "Print",
-          "me": "Print",
-          "ru": "Print",
-          "ua": "Print"
+          "me": "Štampa",
+          "ru": "Печать",
+          "ua": "Друк"
         }
       },
       "step3": {
         "description": {
           "en": "By mail",
-          "me": "By mail",
-          "ru": "By mail",
-          "ua": "By mail"
+          "me": "Dostava kurirom",
+          "ru": "Доставка почтой",
+          "ua": "Доставка поштою"
         },
         "title": {
           "en": "Ship",
-          "me": "Ship",
-          "ru": "Ship",
-          "ua": "Ship"
+          "me": "Dostava",
+          "ru": "Доставка",
+          "ua": "Доставка"
         }
       },
       "step4": {
         "description": {
           "en": "Trust model",
-          "me": "Trust model",
-          "ru": "Trust model",
-          "ua": "Trust model"
+          "me": "Sigurno plaćanje",
+          "ru": "Удобная оплата",
+          "ua": "Зручна оплата"
         },
         "title": {
           "en": "Pay",
-          "me": "Pay",
-          "ru": "Pay",
-          "ua": "Pay"
+          "me": "Plaćanje",
+          "ru": "Оплата",
+          "ua": "Оплата"
         }
       },
       "title": {
@@ -2041,9 +2041,9 @@
     "disclaimer": {
       "text": {
         "en": "These features are due to the very nature of FDM printing technology and do not affect the functionality of the product.",
-        "me": "Ove karakteristike su posledica same prirode tehnologije FDM štampe.",
-        "ru": "Эти особенности обусловлены самой природой технологии FDM-печати.",
-        "ua": "Ці особливості обумовлені самою природою технології FDM-друку."
+        "me": "Ove karakteristike su posledica same prirode tehnologije FDM štampe i ne utiču na funkcionalnost proizvoda.",
+        "ru": "Эти особенности обусловлены самой природой технологии FDM-печати и не влияют на функциональность изделия.",
+        "ua": "Ці особливості обумовлені самою природою технології FDM-друку і не впливають на функціональність виробу."
       },
       "title": {
         "en": "Quality Assurance",
@@ -2434,7 +2434,7 @@
       "en": "This Privacy Policy describes:\n• what data we collect\n• how and why we use it\n• where and how it is stored or transferred\n• your rights regarding your data\n• how to contact us about privacy\n\nBy visiting our site, contacting us, or using our services, you agree to this policy.",
       "me": "Ova Politika privatnosti opisuje:\n• koje podatke prikupljamo\n• kako i zašto ih koristimo\n• gdje se čuvaju i kako se prenose\n• vaša prava u vezi sa vašim podacima\n• kako da nas kontaktirate u vezi sa privatnošću\n\nPosjetom našem sajtu, stupanjem u kontakt sa nama ili korišćenjem naših usluga, prihvatate ovu politiku.",
       "ru": "Эта Политика конфиденциальности описывает:\n• какие данные мы собираем\n• как и почему мы их используем\n• где и как они хранятся или передаются\n• ваши права в отношении ваших данных\n• как связаться с нами по вопросам конфиденциальности\n\nПосещая наш сайт, связываясь с нами или пользуясь нашими услугами, вы соглашаетесь с этой политикой.",
-      "ua": "Ця Політика конфіденційності описує:\n• які дані ми збираємо\n• як і чому ми їх використовуємо\n• де і як вони зберігаються або передаються\n• ваші права щодо ваших даних\n• як зв'язатися з нами щодо конфіденційності\n\nВідвідуючи наш сайт, зв'язуючись з нами або користуючись нашими послугами, вы погоджуєтеся з цією політикою."
+      "ua": "Ця Політика конфіденційності описує:\n• які дані ми збираємо\n• як і чому ми їх використовуємо\n• де і як вони зберігаються або передаються\n• ваші права щодо ваших даних\n• як зв'язатися з нами щодо конфіденційності\n\nВідвідуючи наш сайт, зв'язуючись з нами або користуючись нашими послугами, ви погоджуєтеся з цією політикою."
     },
     "responseNotice": {
       "en": "We respond to all privacy requests within 48 hours.",
@@ -2657,7 +2657,7 @@
         "en": "Maximum resolution and smooth industrial finish.",
         "me": "Maksimalna preciznost i glatka industrijska obrada.",
         "ru": "Максимальная детализация и гладкость изделий.",
-        "ua": "Максимальная детализация and гладкость изделий."
+        "ua": "Максимальна деталізація та гладкість виробів."
       },
       "title": {
         "en": "SLA Resin",

+ 30 - 30
src/locales/ua.json

@@ -298,45 +298,45 @@
         "title": "Формати"
       },
       "requirements": {
-        "normals": "Correct Normals",
-        "scale": "Proper Scale",
+        "normals": "Правильні нормалі",
+        "scale": "Правильний масштаб",
         "title": "Вимоги",
-        "wallThickness": "Min Wall Thickness",
-        "watertight": "Watertight"
+        "wallThickness": "Мін. товщина стінки",
+        "watertight": "Герметичність (Watertight)"
       },
       "title": "Підготовка файлів"
     },
     "help": {
-      "contact": "Contact",
-      "description": "Ask us.",
-      "helpCenter": "Help Center",
-      "title": "Need Help?"
+      "contact": "Зв'язатися",
+      "description": "Задайте нам будь-яке питання про 3D-друк.",
+      "helpCenter": "Центр допомоги",
+      "title": "Потрібна допомога?"
     },
     "materialSelection": {
       "abs": {
-        "bestFor": "Tech parts",
-        "considerations": "Heat resistant",
+        "bestFor": "Технічні деталі",
+        "considerations": "Термостійкий",
         "name": "ABS"
       },
       "petg": {
-        "bestFor": "Durable",
-        "considerations": "Easy print",
+        "bestFor": "Міцні вироби",
+        "considerations": "Легкий друк",
         "name": "PETG"
       },
       "pla": {
-        "bestFor": "Prototypes",
-        "considerations": "Bio-degradable",
+        "bestFor": "Прототипи",
+        "considerations": "Біорозкладний",
         "name": "PLA"
       },
       "resin": {
-        "bestFor": "Detail",
-        "considerations": "UV sensitive",
+        "bestFor": "Дрібні деталі",
+        "considerations": "УФ-чутливий",
         "name": "Resin"
       },
       "table": {
-        "bestFor": "Best For",
-        "considerations": "Notes",
-        "material": "Material"
+        "bestFor": "Підходить для",
+        "considerations": "Особливості",
+        "material": "Матеріал"
       },
       "title": "Матеріали"
     },
@@ -393,20 +393,20 @@
     },
     "orderingProcess": {
       "step1": {
-        "description": "Send file",
-        "title": "Upload"
+        "description": "Завантажте файл",
+        "title": "Завантаження"
       },
       "step2": {
-        "description": "We craft",
-        "title": "Print"
+        "description": "Ми виготовляємо",
+        "title": "Друк"
       },
       "step3": {
-        "description": "By mail",
-        "title": "Ship"
+        "description": "Доставка поштою",
+        "title": "Доставка"
       },
       "step4": {
-        "description": "Trust model",
-        "title": "Pay"
+        "description": "Зручна оплата",
+        "title": "Оплата"
       },
       "title": "Process"
     },
@@ -504,7 +504,7 @@
     "aiDisclaimer": "Усі пояснювальні фотографії згенеровані ШІ для наочності.",
     "description": "Вироби, виготовлені методом FDM (пошарового наплавлення пластику), мають ряд візуальних і тактильних особливостей, які є нормою технології та не вважаються дефектами.",
     "disclaimer": {
-      "text": "Ці особливості обумовлені самою природою технології FDM-друку.",
+      "text": "Ці особливості обумовлені самою природою технології FDM-друку і не впливають на функціональність виробу.",
       "title": "Гарантія якості"
     },
     "items": {
@@ -595,7 +595,7 @@
   "privacy": {
     "contactDesc": "Якщо у вас є питання, наша команда завжди готова допомогти.",
     "contactTitle": "Потрібна допомога?",
-    "intro": "Ця Політика конфіденційності описує:\n• які дані ми збираємо\n• як і чому ми їх використовуємо\n• де і як вони зберігаються або передаються\n• ваші права щодо ваших даних\n• як зв'язатися з нами щодо конфіденційності\n\nВідвідуючи наш сайт, зв'язуючись з нами або користуючись нашими послугами, вы погоджуєтеся з цією політикою.",
+    "intro": "Ця Політика конфіденційності описує:\n• які дані ми збираємо\n• як і чому ми їх використовуємо\n• де і як вони зберігаються або передаються\n• ваші права щодо ваших даних\n• як зв'язатися з нами щодо конфіденційності\n\nВідвідуючи наш сайт, зв'язуючись з нами або користуючись нашими послугами, ви погоджуєтеся з цією політикою.",
     "responseNotice": "Ми відповідаємо на всі запити протягом 48 годин.",
     "sections": {
       "01_data": {
@@ -658,7 +658,7 @@
       "title": "FDM друк"
     },
     "sla": {
-      "description": "Максимальная детализация and гладкость изделий.",
+      "description": "Максимальна деталізація та гладкість виробів.",
       "title": "SLA смола"
     },
     "title": "Технології",

+ 64 - 11
src/pages/Auth.vue

@@ -188,17 +188,8 @@
                 <span class="bg-card/40 px-2 text-muted-foreground backdrop-blur-md">{{ t("auth.orContinueWith") }}</span>
               </div>
             </div>
-            <div class="grid grid-cols-2 gap-3">
-              <button type="button" @click="toast.info(t('auth.toasts.socialSoon', { provider: 'Google' }))"
-                class="flex items-center justify-center gap-2 bg-background/50 hover:bg-background/80 border border-border/50 rounded-xl py-2.5 px-4 transition-all hover:scale-[1.02] active:scale-[0.98]">
-                <svg class="w-4 h-4" viewBox="0 0 24 24">
-                  <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
-                  <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
-                  <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
-                  <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
-                </svg>
-                <span class="text-xs font-medium">Google</span>
-              </button>
+            <div class="grid grid-cols-1 gap-3">
+              <div id="google-login-btn" class="w-full flex justify-center"></div>
               <button type="button" @click="toast.info(t('auth.toasts.socialSoon', { provider: 'Facebook' }))"
                 class="flex items-center justify-center gap-2 bg-background/50 hover:bg-background/80 border border-border/50 rounded-xl py-2.5 px-4 transition-all hover:scale-[1.02] active:scale-[0.98]">
                 <svg class="w-4 h-4 fill-[#1877F2]" viewBox="0 0 24 24">
@@ -262,8 +253,70 @@ const formData = reactive({
 onMounted(() => {
   const token = route.query.token as string;
   if (token) { mode.value = "reset"; formData.token = token; }
+  
+  // Load Google Identity Services script
+  const script = document.createElement('script');
+  script.src = 'https://accounts.google.com/gsi/client';
+  script.async = true;
+  script.defer = true;
+  script.onload = initGoogleLogin;
+  document.head.appendChild(script);
 });
 
+function initGoogleLogin() {
+  const google = (window as any).google;
+  if (!google) return;
+
+  const clientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
+  if (!clientId) {
+    console.warn("GOOGLE_CLIENT_ID not found in environment");
+    return;
+  }
+
+  google.accounts.id.initialize({
+    client_id: clientId,
+    callback: handleGoogleResponse,
+    auto_select: false,
+    cancel_on_tap_outside: true,
+  });
+
+  google.accounts.id.renderButton(
+    document.getElementById("google-login-btn"),
+    { 
+      type: "standard",
+      theme: "outline", 
+      size: "large", 
+      text: "continue_with",
+      shape: "rectangular",
+      logo_alignment: "left",
+      width: "100%"
+    }
+  );
+}
+
+const { socialLogin } = await import("@/lib/api");
+
+async function handleGoogleResponse(response: any) {
+  isLoading.value = true;
+  try {
+    const res = await socialLogin({
+      provider: "google",
+      token: response.credential,
+      email: "", // Backend will extract from token
+      preferred_language: currentLanguage()
+    });
+    
+    localStorage.setItem("token", res.access_token);
+    await authStore.refreshUser();
+    toast.success(t("auth.toasts.welcomeBack"));
+    router.push("/");
+  } catch (err: any) {
+    toast.error(err.message || "Google Login failed");
+  } finally {
+    isLoading.value = false;
+  }
+}
+
 function getTitle() {
   return { login: t("auth.login.title"), register: t("auth.register.title"), forgot: t("auth.forgot.title"), reset: t("auth.reset.title") }[mode.value];
 }