feat: 完整实现中英文双语支持并修复所有声音翻译问题

## 主要功能
- 实现完整的中英文双语支持系统
- 添加语言切换器和路由配置
- 创建统一的翻译文件和hooks

## 核心组件
- 新增语言切换器组件
- 实现中英文页面路由
- 添加翻译系统核心文件

## 翻译修复
- 修复所有声音名称的dataI18n映射
- 解决重复翻译键冲突问题
- 完善所有分类的声音翻译

## 声音分类优化
- 修复雨声分类的重复翻译键问题
- 清理跨分类翻译键冲突
- 优化声音分类归属

## UI优化
- 移除页面底部开源模块
- 完善顶部捐赠文本翻译
- 优化所有菜单项的翻译显示
This commit is contained in:
zl 2025-11-16 14:30:33 +08:00
parent 6ac65c1948
commit 65958f8482
62 changed files with 2757 additions and 108 deletions

1
.serena/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/cache

84
.serena/project.yml Normal file
View file

@ -0,0 +1,84 @@
# list of languages for which language servers are started; choose from:
# al bash clojure cpp csharp csharp_omnisharp
# dart elixir elm erlang fortran go
# haskell java julia kotlin lua markdown
# nix perl php python python_jedi r
# rego ruby ruby_solargraph rust scala swift
# terraform typescript typescript_vts zig
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# Special requirements:
# - csharp: Requires the presence of a .sln file in the project folder.
# When using multiple languages, the first language server that supports a given file will be used for that file.
# The first language is the default language and the respective language server will be used as a fallback.
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
languages:
- typescript
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: "utf-8"
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "moodist"
included_optional_tools: []

View file

@ -0,0 +1,96 @@
# Design Document
## Overview
[High-level description of the feature and its place in the overall system]
## Steering Document Alignment
### Technical Standards (tech.md)
[How the design follows documented technical patterns and standards]
### Project Structure (structure.md)
[How the implementation will follow project organization conventions]
## Code Reuse Analysis
[What existing code will be leveraged, extended, or integrated with this feature]
### Existing Components to Leverage
- **[Component/Utility Name]**: [How it will be used]
- **[Service/Helper Name]**: [How it will be extended]
### Integration Points
- **[Existing System/API]**: [How the new feature will integrate]
- **[Database/Storage]**: [How data will connect to existing schemas]
## Architecture
[Describe the overall architecture and design patterns used]
### Modular Design Principles
- **Single File Responsibility**: Each file should handle one specific concern or domain
- **Component Isolation**: Create small, focused components rather than large monolithic files
- **Service Layer Separation**: Separate data access, business logic, and presentation layers
- **Utility Modularity**: Break utilities into focused, single-purpose modules
```mermaid
graph TD
A[Component A] --> B[Component B]
B --> C[Component C]
```
## Components and Interfaces
### Component 1
- **Purpose:** [What this component does]
- **Interfaces:** [Public methods/APIs]
- **Dependencies:** [What it depends on]
- **Reuses:** [Existing components/utilities it builds upon]
### Component 2
- **Purpose:** [What this component does]
- **Interfaces:** [Public methods/APIs]
- **Dependencies:** [What it depends on]
- **Reuses:** [Existing components/utilities it builds upon]
## Data Models
### Model 1
```
[Define the structure of Model1 in your language]
- id: [unique identifier type]
- name: [string/text type]
- [Additional properties as needed]
```
### Model 2
```
[Define the structure of Model2 in your language]
- id: [unique identifier type]
- [Additional properties as needed]
```
## Error Handling
### Error Scenarios
1. **Scenario 1:** [Description]
- **Handling:** [How to handle]
- **User Impact:** [What user sees]
2. **Scenario 2:** [Description]
- **Handling:** [How to handle]
- **User Impact:** [What user sees]
## Testing Strategy
### Unit Testing
- [Unit testing approach]
- [Key components to test]
### Integration Testing
- [Integration testing approach]
- [Key flows to test]
### End-to-End Testing
- [E2E testing approach]
- [User scenarios to test]

View file

@ -0,0 +1,51 @@
# Product Overview
## Product Purpose
[Describe the core purpose of this product/project. What problem does it solve?]
## Target Users
[Who are the primary users of this product? What are their needs and pain points?]
## Key Features
[List the main features that deliver value to users]
1. **Feature 1**: [Description]
2. **Feature 2**: [Description]
3. **Feature 3**: [Description]
## Business Objectives
[What are the business goals this product aims to achieve?]
- [Objective 1]
- [Objective 2]
- [Objective 3]
## Success Metrics
[How will we measure the success of this product?]
- [Metric 1]: [Target]
- [Metric 2]: [Target]
- [Metric 3]: [Target]
## Product Principles
[Core principles that guide product decisions]
1. **[Principle 1]**: [Explanation]
2. **[Principle 2]**: [Explanation]
3. **[Principle 3]**: [Explanation]
## Monitoring & Visibility (if applicable)
[How do users track progress and monitor the system?]
- **Dashboard Type**: [e.g., Web-based, CLI, Desktop app]
- **Real-time Updates**: [e.g., WebSocket, polling, push notifications]
- **Key Metrics Displayed**: [What information is most important to surface]
- **Sharing Capabilities**: [e.g., read-only links, exports, reports]
## Future Vision
[Where do we see this product evolving in the future?]
### Potential Enhancements
- **Remote Access**: [e.g., Tunnel features for sharing dashboards with stakeholders]
- **Analytics**: [e.g., Historical trends, performance metrics]
- **Collaboration**: [e.g., Multi-user support, commenting]

View file

@ -0,0 +1,50 @@
# Requirements Document
## Introduction
[Provide a brief overview of the feature, its purpose, and its value to users]
## Alignment with Product Vision
[Explain how this feature supports the goals outlined in product.md]
## Requirements
### Requirement 1
**User Story:** As a [role], I want [feature], so that [benefit]
#### Acceptance Criteria
1. WHEN [event] THEN [system] SHALL [response]
2. IF [precondition] THEN [system] SHALL [response]
3. WHEN [event] AND [condition] THEN [system] SHALL [response]
### Requirement 2
**User Story:** As a [role], I want [feature], so that [benefit]
#### Acceptance Criteria
1. WHEN [event] THEN [system] SHALL [response]
2. IF [precondition] THEN [system] SHALL [response]
## Non-Functional Requirements
### Code Architecture and Modularity
- **Single Responsibility Principle**: Each file should have a single, well-defined purpose
- **Modular Design**: Components, utilities, and services should be isolated and reusable
- **Dependency Management**: Minimize interdependencies between modules
- **Clear Interfaces**: Define clean contracts between components and layers
### Performance
- [Performance requirements]
### Security
- [Security requirements]
### Reliability
- [Reliability requirements]
### Usability
- [Usability requirements]

View file

@ -0,0 +1,145 @@
# Project Structure
## Directory Organization
```
[Define your project's directory structure. Examples below - adapt to your project type]
Example for a library/package:
project-root/
├── src/ # Source code
├── tests/ # Test files
├── docs/ # Documentation
├── examples/ # Usage examples
└── [build/dist/out] # Build output
Example for an application:
project-root/
├── [src/app/lib] # Main source code
├── [assets/resources] # Static resources
├── [config/settings] # Configuration
├── [scripts/tools] # Build/utility scripts
└── [tests/spec] # Test files
Common patterns:
- Group by feature/module
- Group by layer (UI, business logic, data)
- Group by type (models, controllers, views)
- Flat structure for simple projects
```
## Naming Conventions
### Files
- **Components/Modules**: [e.g., `PascalCase`, `snake_case`, `kebab-case`]
- **Services/Handlers**: [e.g., `UserService`, `user_service`, `user-service`]
- **Utilities/Helpers**: [e.g., `dateUtils`, `date_utils`, `date-utils`]
- **Tests**: [e.g., `[filename]_test`, `[filename].test`, `[filename]Test`]
### Code
- **Classes/Types**: [e.g., `PascalCase`, `CamelCase`, `snake_case`]
- **Functions/Methods**: [e.g., `camelCase`, `snake_case`, `PascalCase`]
- **Constants**: [e.g., `UPPER_SNAKE_CASE`, `SCREAMING_CASE`, `PascalCase`]
- **Variables**: [e.g., `camelCase`, `snake_case`, `lowercase`]
## Import Patterns
### Import Order
1. External dependencies
2. Internal modules
3. Relative imports
4. Style imports
### Module/Package Organization
```
[Describe your project's import/include patterns]
Examples:
- Absolute imports from project root
- Relative imports within modules
- Package/namespace organization
- Dependency management approach
```
## Code Structure Patterns
[Define common patterns for organizing code within files. Below are examples - choose what applies to your project]
### Module/Class Organization
```
Example patterns:
1. Imports/includes/dependencies
2. Constants and configuration
3. Type/interface definitions
4. Main implementation
5. Helper/utility functions
6. Exports/public API
```
### Function/Method Organization
```
Example patterns:
- Input validation first
- Core logic in the middle
- Error handling throughout
- Clear return points
```
### File Organization Principles
```
Choose what works for your project:
- One class/module per file
- Related functionality grouped together
- Public API at the top/bottom
- Implementation details hidden
```
## Code Organization Principles
1. **Single Responsibility**: Each file should have one clear purpose
2. **Modularity**: Code should be organized into reusable modules
3. **Testability**: Structure code to be easily testable
4. **Consistency**: Follow patterns established in the codebase
## Module Boundaries
[Define how different parts of your project interact and maintain separation of concerns]
Examples of boundary patterns:
- **Core vs Plugins**: Core functionality vs extensible plugins
- **Public API vs Internal**: What's exposed vs implementation details
- **Platform-specific vs Cross-platform**: OS-specific code isolation
- **Stable vs Experimental**: Production code vs experimental features
- **Dependencies direction**: Which modules can depend on which
## Code Size Guidelines
[Define your project's guidelines for file and function sizes]
Suggested guidelines:
- **File size**: [Define maximum lines per file]
- **Function/Method size**: [Define maximum lines per function]
- **Class/Module complexity**: [Define complexity limits]
- **Nesting depth**: [Maximum nesting levels]
## Dashboard/Monitoring Structure (if applicable)
[How dashboard or monitoring components are organized]
### Example Structure:
```
src/
└── dashboard/ # Self-contained dashboard subsystem
├── server/ # Backend server components
├── client/ # Frontend assets
├── shared/ # Shared types/utilities
└── public/ # Static assets
```
### Separation of Concerns
- Dashboard isolated from core business logic
- Own CLI entry point for independent operation
- Minimal dependencies on main application
- Can be disabled without affecting core functionality
## Documentation Standards
- All public APIs must have documentation
- Complex logic should include inline comments
- README files for major modules
- Follow language-specific documentation conventions

View file

@ -0,0 +1,139 @@
# Tasks Document
- [ ] 1. Create core interfaces in src/types/feature.ts
- File: src/types/feature.ts
- Define TypeScript interfaces for feature data structures
- Extend existing base interfaces from base.ts
- Purpose: Establish type safety for feature implementation
- _Leverage: src/types/base.ts_
- _Requirements: 1.1_
- _Prompt: Role: TypeScript Developer specializing in type systems and interfaces | Task: Create comprehensive TypeScript interfaces for the feature data structures following requirements 1.1, extending existing base interfaces from src/types/base.ts | Restrictions: Do not modify existing base interfaces, maintain backward compatibility, follow project naming conventions | Success: All interfaces compile without errors, proper inheritance from base types, full type coverage for feature requirements_
- [ ] 2. Create base model class in src/models/FeatureModel.ts
- File: src/models/FeatureModel.ts
- Implement base model extending BaseModel class
- Add validation methods using existing validation utilities
- Purpose: Provide data layer foundation for feature
- _Leverage: src/models/BaseModel.ts, src/utils/validation.ts_
- _Requirements: 2.1_
- _Prompt: Role: Backend Developer with expertise in Node.js and data modeling | Task: Create a base model class extending BaseModel and implementing validation following requirement 2.1, leveraging existing patterns from src/models/BaseModel.ts and src/utils/validation.ts | Restrictions: Must follow existing model patterns, do not bypass validation utilities, maintain consistent error handling | Success: Model extends BaseModel correctly, validation methods implemented and tested, follows project architecture patterns_
- [ ] 3. Add specific model methods to FeatureModel.ts
- File: src/models/FeatureModel.ts (continue from task 2)
- Implement create, update, delete methods
- Add relationship handling for foreign keys
- Purpose: Complete model functionality for CRUD operations
- _Leverage: src/models/BaseModel.ts_
- _Requirements: 2.2, 2.3_
- _Prompt: Role: Backend Developer with expertise in ORM and database operations | Task: Implement CRUD methods and relationship handling in FeatureModel.ts following requirements 2.2 and 2.3, extending patterns from src/models/BaseModel.ts | Restrictions: Must maintain transaction integrity, follow existing relationship patterns, do not duplicate base model functionality | Success: All CRUD operations work correctly, relationships are properly handled, database operations are atomic and efficient_
- [ ] 4. Create model unit tests in tests/models/FeatureModel.test.ts
- File: tests/models/FeatureModel.test.ts
- Write tests for model validation and CRUD methods
- Use existing test utilities and fixtures
- Purpose: Ensure model reliability and catch regressions
- _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_
- _Requirements: 2.1, 2.2_
- _Prompt: Role: QA Engineer with expertise in unit testing and Jest/Mocha frameworks | Task: Create comprehensive unit tests for FeatureModel validation and CRUD methods covering requirements 2.1 and 2.2, using existing test utilities from tests/helpers/testUtils.ts and fixtures from tests/fixtures/data.ts | Restrictions: Must test both success and failure scenarios, do not test external dependencies directly, maintain test isolation | Success: All model methods are tested with good coverage, edge cases covered, tests run independently and consistently_
- [ ] 5. Create service interface in src/services/IFeatureService.ts
- File: src/services/IFeatureService.ts
- Define service contract with method signatures
- Extend base service interface patterns
- Purpose: Establish service layer contract for dependency injection
- _Leverage: src/services/IBaseService.ts_
- _Requirements: 3.1_
- _Prompt: Role: Software Architect specializing in service-oriented architecture and TypeScript interfaces | Task: Design service interface contract following requirement 3.1, extending base service patterns from src/services/IBaseService.ts for dependency injection | Restrictions: Must maintain interface segregation principle, do not expose internal implementation details, ensure contract compatibility with DI container | Success: Interface is well-defined with clear method signatures, extends base service appropriately, supports all required service operations_
- [ ] 6. Implement feature service in src/services/FeatureService.ts
- File: src/services/FeatureService.ts
- Create concrete service implementation using FeatureModel
- Add error handling with existing error utilities
- Purpose: Provide business logic layer for feature operations
- _Leverage: src/services/BaseService.ts, src/utils/errorHandler.ts, src/models/FeatureModel.ts_
- _Requirements: 3.2_
- _Prompt: Role: Backend Developer with expertise in service layer architecture and business logic | Task: Implement concrete FeatureService following requirement 3.2, using FeatureModel and extending BaseService patterns with proper error handling from src/utils/errorHandler.ts | Restrictions: Must implement interface contract exactly, do not bypass model validation, maintain separation of concerns from data layer | Success: Service implements all interface methods correctly, robust error handling implemented, business logic is well-encapsulated and testable_
- [ ] 7. Add service dependency injection in src/utils/di.ts
- File: src/utils/di.ts (modify existing)
- Register FeatureService in dependency injection container
- Configure service lifetime and dependencies
- Purpose: Enable service injection throughout application
- _Leverage: existing DI configuration in src/utils/di.ts_
- _Requirements: 3.1_
- _Prompt: Role: DevOps Engineer with expertise in dependency injection and IoC containers | Task: Register FeatureService in DI container following requirement 3.1, configuring appropriate lifetime and dependencies using existing patterns from src/utils/di.ts | Restrictions: Must follow existing DI container patterns, do not create circular dependencies, maintain service resolution efficiency | Success: FeatureService is properly registered and resolvable, dependencies are correctly configured, service lifetime is appropriate for use case_
- [ ] 8. Create service unit tests in tests/services/FeatureService.test.ts
- File: tests/services/FeatureService.test.ts
- Write tests for service methods with mocked dependencies
- Test error handling scenarios
- Purpose: Ensure service reliability and proper error handling
- _Leverage: tests/helpers/testUtils.ts, tests/mocks/modelMocks.ts_
- _Requirements: 3.2, 3.3_
- _Prompt: Role: QA Engineer with expertise in service testing and mocking frameworks | Task: Create comprehensive unit tests for FeatureService methods covering requirements 3.2 and 3.3, using mocked dependencies from tests/mocks/modelMocks.ts and test utilities | Restrictions: Must mock all external dependencies, test business logic in isolation, do not test framework code | Success: All service methods tested with proper mocking, error scenarios covered, tests verify business logic correctness and error handling_
- [ ] 4. Create API endpoints
- Design API structure
- _Leverage: src/api/baseApi.ts, src/utils/apiUtils.ts_
- _Requirements: 4.0_
- _Prompt: Role: API Architect specializing in RESTful design and Express.js | Task: Design comprehensive API structure following requirement 4.0, leveraging existing patterns from src/api/baseApi.ts and utilities from src/utils/apiUtils.ts | Restrictions: Must follow REST conventions, maintain API versioning compatibility, do not expose internal data structures directly | Success: API structure is well-designed and documented, follows existing patterns, supports all required operations with proper HTTP methods and status codes_
- [ ] 4.1 Set up routing and middleware
- Configure application routes
- Add authentication middleware
- Set up error handling middleware
- _Leverage: src/middleware/auth.ts, src/middleware/errorHandler.ts_
- _Requirements: 4.1_
- _Prompt: Role: Backend Developer with expertise in Express.js middleware and routing | Task: Configure application routes and middleware following requirement 4.1, integrating authentication from src/middleware/auth.ts and error handling from src/middleware/errorHandler.ts | Restrictions: Must maintain middleware order, do not bypass security middleware, ensure proper error propagation | Success: Routes are properly configured with correct middleware chain, authentication works correctly, errors are handled gracefully throughout the request lifecycle_
- [ ] 4.2 Implement CRUD endpoints
- Create API endpoints
- Add request validation
- Write API integration tests
- _Leverage: src/controllers/BaseController.ts, src/utils/validation.ts_
- _Requirements: 4.2, 4.3_
- _Prompt: Role: Full-stack Developer with expertise in API development and validation | Task: Implement CRUD endpoints following requirements 4.2 and 4.3, extending BaseController patterns and using validation utilities from src/utils/validation.ts | Restrictions: Must validate all inputs, follow existing controller patterns, ensure proper HTTP status codes and responses | Success: All CRUD operations work correctly, request validation prevents invalid data, integration tests pass and cover all endpoints_
- [ ] 5. Add frontend components
- Plan component architecture
- _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_
- _Requirements: 5.0_
- _Prompt: Role: Frontend Architect with expertise in React component design and architecture | Task: Plan comprehensive component architecture following requirement 5.0, leveraging base patterns from src/components/BaseComponent.tsx and theme system from src/styles/theme.ts | Restrictions: Must follow existing component patterns, maintain design system consistency, ensure component reusability | Success: Architecture is well-planned and documented, components are properly organized, follows existing patterns and theme system_
- [ ] 5.1 Create base UI components
- Set up component structure
- Implement reusable components
- Add styling and theming
- _Leverage: src/components/BaseComponent.tsx, src/styles/theme.ts_
- _Requirements: 5.1_
- _Prompt: Role: Frontend Developer specializing in React and component architecture | Task: Create reusable UI components following requirement 5.1, extending BaseComponent patterns and using existing theme system from src/styles/theme.ts | Restrictions: Must use existing theme variables, follow component composition patterns, ensure accessibility compliance | Success: Components are reusable and properly themed, follow existing architecture, accessible and responsive_
- [ ] 5.2 Implement feature-specific components
- Create feature components
- Add state management
- Connect to API endpoints
- _Leverage: src/hooks/useApi.ts, src/components/BaseComponent.tsx_
- _Requirements: 5.2, 5.3_
- _Prompt: Role: React Developer with expertise in state management and API integration | Task: Implement feature-specific components following requirements 5.2 and 5.3, using API hooks from src/hooks/useApi.ts and extending BaseComponent patterns | Restrictions: Must use existing state management patterns, handle loading and error states properly, maintain component performance | Success: Components are fully functional with proper state management, API integration works smoothly, user experience is responsive and intuitive_
- [ ] 6. Integration and testing
- Plan integration approach
- _Leverage: src/utils/integrationUtils.ts, tests/helpers/testUtils.ts_
- _Requirements: 6.0_
- _Prompt: Role: Integration Engineer with expertise in system integration and testing strategies | Task: Plan comprehensive integration approach following requirement 6.0, leveraging integration utilities from src/utils/integrationUtils.ts and test helpers | Restrictions: Must consider all system components, ensure proper test coverage, maintain integration test reliability | Success: Integration plan is comprehensive and feasible, all system components work together correctly, integration points are well-tested_
- [ ] 6.1 Write end-to-end tests
- Set up E2E testing framework
- Write user journey tests
- Add test automation
- _Leverage: tests/helpers/testUtils.ts, tests/fixtures/data.ts_
- _Requirements: All_
- _Prompt: Role: QA Automation Engineer with expertise in E2E testing and test frameworks like Cypress or Playwright | Task: Implement comprehensive end-to-end tests covering all requirements, setting up testing framework and user journey tests using test utilities and fixtures | Restrictions: Must test real user workflows, ensure tests are maintainable and reliable, do not test implementation details | Success: E2E tests cover all critical user journeys, tests run reliably in CI/CD pipeline, user experience is validated from end-to-end_
- [ ] 6.2 Final integration and cleanup
- Integrate all components
- Fix any integration issues
- Clean up code and documentation
- _Leverage: src/utils/cleanup.ts, docs/templates/_
- _Requirements: All_
- _Prompt: Role: Senior Developer with expertise in code quality and system integration | Task: Complete final integration of all components and perform comprehensive cleanup covering all requirements, using cleanup utilities and documentation templates | Restrictions: Must not break existing functionality, ensure code quality standards are met, maintain documentation consistency | Success: All components are fully integrated and working together, code is clean and well-documented, system meets all requirements and quality standards_

View file

@ -0,0 +1,99 @@
# Technology Stack
## Project Type
[Describe what kind of project this is: web application, CLI tool, desktop application, mobile app, library, API service, embedded system, game, etc.]
## Core Technologies
### Primary Language(s)
- **Language**: [e.g., Python 3.11, Go 1.21, TypeScript, Rust, C++]
- **Runtime/Compiler**: [if applicable]
- **Language-specific tools**: [package managers, build tools, etc.]
### Key Dependencies/Libraries
[List the main libraries and frameworks your project depends on]
- **[Library/Framework name]**: [Purpose and version]
- **[Library/Framework name]**: [Purpose and version]
### Application Architecture
[Describe how your application is structured - this could be MVC, event-driven, plugin-based, client-server, standalone, microservices, monolithic, etc.]
### Data Storage (if applicable)
- **Primary storage**: [e.g., PostgreSQL, files, in-memory, cloud storage]
- **Caching**: [e.g., Redis, in-memory, disk cache]
- **Data formats**: [e.g., JSON, Protocol Buffers, XML, binary]
### External Integrations (if applicable)
- **APIs**: [External services you integrate with]
- **Protocols**: [e.g., HTTP/REST, gRPC, WebSocket, TCP/IP]
- **Authentication**: [e.g., OAuth, API keys, certificates]
### Monitoring & Dashboard Technologies (if applicable)
- **Dashboard Framework**: [e.g., React, Vue, vanilla JS, terminal UI]
- **Real-time Communication**: [e.g., WebSocket, Server-Sent Events, polling]
- **Visualization Libraries**: [e.g., Chart.js, D3, terminal graphs]
- **State Management**: [e.g., Redux, Vuex, file system as source of truth]
## Development Environment
### Build & Development Tools
- **Build System**: [e.g., Make, CMake, Gradle, npm scripts, cargo]
- **Package Management**: [e.g., pip, npm, cargo, go mod, apt, brew]
- **Development workflow**: [e.g., hot reload, watch mode, REPL]
### Code Quality Tools
- **Static Analysis**: [Tools for code quality and correctness]
- **Formatting**: [Code style enforcement tools]
- **Testing Framework**: [Unit, integration, and/or end-to-end testing tools]
- **Documentation**: [Documentation generation tools]
### Version Control & Collaboration
- **VCS**: [e.g., Git, Mercurial, SVN]
- **Branching Strategy**: [e.g., Git Flow, GitHub Flow, trunk-based]
- **Code Review Process**: [How code reviews are conducted]
### Dashboard Development (if applicable)
- **Live Reload**: [e.g., Hot module replacement, file watchers]
- **Port Management**: [e.g., Dynamic allocation, configurable ports]
- **Multi-Instance Support**: [e.g., Running multiple dashboards simultaneously]
## Deployment & Distribution (if applicable)
- **Target Platform(s)**: [Where/how the project runs: cloud, on-premise, desktop, mobile, embedded]
- **Distribution Method**: [How users get your software: download, package manager, app store, SaaS]
- **Installation Requirements**: [Prerequisites, system requirements]
- **Update Mechanism**: [How updates are delivered]
## Technical Requirements & Constraints
### Performance Requirements
- [e.g., response time, throughput, memory usage, startup time]
- [Specific benchmarks or targets]
### Compatibility Requirements
- **Platform Support**: [Operating systems, architectures, versions]
- **Dependency Versions**: [Minimum/maximum versions of dependencies]
- **Standards Compliance**: [Industry standards, protocols, specifications]
### Security & Compliance
- **Security Requirements**: [Authentication, encryption, data protection]
- **Compliance Standards**: [GDPR, HIPAA, SOC2, etc. if applicable]
- **Threat Model**: [Key security considerations]
### Scalability & Reliability
- **Expected Load**: [Users, requests, data volume]
- **Availability Requirements**: [Uptime targets, disaster recovery]
- **Growth Projections**: [How the system needs to scale]
## Technical Decisions & Rationale
[Document key architectural and technology choices]
### Decision Log
1. **[Technology/Pattern Choice]**: [Why this was chosen, alternatives considered]
2. **[Architecture Decision]**: [Rationale, trade-offs accepted]
3. **[Tool/Library Selection]**: [Reasoning, evaluation criteria]
## Known Limitations
[Document any technical debt, limitations, or areas for improvement]
- [Limitation 1]: [Impact and potential future solutions]
- [Limitation 2]: [Why it exists and when it might be addressed]

View file

@ -0,0 +1,64 @@
# User Templates
This directory allows you to create custom templates that override the default Spec Workflow templates.
## How to Use Custom Templates
1. **Create your custom template file** in this directory with the exact same name as the default template you want to override:
- `requirements-template.md` - Override requirements document template
- `design-template.md` - Override design document template
- `tasks-template.md` - Override tasks document template
- `product-template.md` - Override product steering template
- `tech-template.md` - Override tech steering template
- `structure-template.md` - Override structure steering template
2. **Template Loading Priority**:
- The system first checks this `user-templates/` directory
- If a matching template is found here, it will be used
- Otherwise, the default template from `templates/` will be used
## Example Custom Template
To create a custom requirements template:
1. Create a file named `requirements-template.md` in this directory
2. Add your custom structure, for example:
```markdown
# Requirements Document
## Executive Summary
[Your custom section]
## Business Requirements
[Your custom structure]
## Technical Requirements
[Your custom fields]
## Custom Sections
[Add any sections specific to your workflow]
```
## Template Variables
Templates can include placeholders that will be replaced when documents are created:
- `{{projectName}}` - The name of your project
- `{{featureName}}` - The name of the feature being specified
- `{{date}}` - The current date
- `{{author}}` - The document author
## Best Practices
1. **Start from defaults**: Copy a default template from `../templates/` as a starting point
2. **Keep structure consistent**: Maintain similar section headers for tool compatibility
3. **Document changes**: Add comments explaining why sections were added/modified
4. **Version control**: Track your custom templates in version control
5. **Test thoroughly**: Ensure custom templates work with the spec workflow tools
## Notes
- Custom templates are project-specific and not included in the package distribution
- The `templates/` directory contains the default templates which are updated with each version
- Your custom templates in this directory are preserved during updates
- If a custom template has errors, the system will fall back to the default template

View file

@ -33,4 +33,9 @@ export default defineConfig({
},
}),
],
vite: {
define: {
global: 'globalThis',
},
},
});

672
package-lock.json generated
View file

@ -20,15 +20,18 @@
"@types/react-dom": "^18.2.10",
"@vite-pwa/astro": "0.5.0",
"astro": "4.10.3",
"astro-i18next": "1.0.0-beta.21",
"deepmerge": "4.3.1",
"focus-trap-react": "10.2.3",
"framer-motion": "10.16.4",
"howler": "2.2.4",
"i18next": "25.6.2",
"js-confetti": "0.12.0",
"motion": "12.23.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "3.2.1",
"react-i18next": "16.3.3",
"react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0",
"react-youtube": "10.1.0",
@ -115,7 +118,8 @@
"node_modules/@astrojs/compiler": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.8.0.tgz",
"integrity": "sha512-yrpD1WRGqsJwANaDIdtHo+YVjvIOFAjC83lu5qENIgrafwZcJgSXDuwVMXOgok4tFzpeKLsFQ6c3FoUdloLWBQ=="
"integrity": "sha512-yrpD1WRGqsJwANaDIdtHo+YVjvIOFAjC83lu5qENIgrafwZcJgSXDuwVMXOgok4tFzpeKLsFQ6c3FoUdloLWBQ==",
"peer": true
},
"node_modules/@astrojs/internal-helpers": {
"version": "0.4.0",
@ -229,6 +233,7 @@
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
"integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@ -2044,12 +2049,10 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
},
"node_modules/@babel/runtime": {
"version": "7.23.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
"integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@ -2889,6 +2892,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@ -2911,6 +2915,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@ -4288,6 +4293,29 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@proload/core": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@proload/core/-/core-0.3.3.tgz",
"integrity": "sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"deepmerge": "^4.2.2",
"escalade": "^3.1.1"
}
},
"node_modules/@proload/plugin-tsm": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@proload/plugin-tsm/-/plugin-tsm-0.2.1.tgz",
"integrity": "sha512-Ex1sL2BxU+g8MHdAdq9SZKz+pU34o8Zcl9PHWo2WaG9hrnlZme607PU6gnpoAYsDBpHX327+eu60wWUk+d/b+A==",
"license": "MIT",
"dependencies": {
"tsm": "^2.1.4"
},
"peerDependencies": {
"@proload/core": "^0.3.2"
}
},
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@ -8850,7 +8878,8 @@
"version": "20.5.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.1.tgz",
"integrity": "sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg==",
"devOptional": true
"devOptional": true,
"peer": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.2",
@ -8885,6 +8914,7 @@
"version": "18.2.25",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.25.tgz",
"integrity": "sha512-24xqse6+VByVLIr+xWaQ9muX1B4bXJKXBbjszbld/UEDslGLY53+ZucF44HCmLbMPejTzGG9XgR+3m2/Wqu1kw==",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -8895,6 +8925,7 @@
"version": "18.2.10",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.10.tgz",
"integrity": "sha512-5VEC5RgXIk1HHdyN1pHlg0cOqnxHzvPGpMMyGAP5qSaDRmyZNDaQ0kkVAkK6NYlDhP6YBID3llaXlmAS/mdgCA==",
"peer": true,
"dependencies": {
"@types/react": "*"
}
@ -9135,6 +9166,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz",
"integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.7.4",
"@typescript-eslint/types": "6.7.4",
@ -9591,6 +9623,7 @@
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -10025,6 +10058,7 @@
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/astro/-/astro-4.10.3.tgz",
"integrity": "sha512-TWCJM+Vg+y0UoEz/H75rfp/u2N8yxeQQ2UrU9+fMcbjlzQJtGGDq3ApdundqPZgAuCryRuJnrKytStMZCFnlvQ==",
"peer": true,
"dependencies": {
"@astrojs/compiler": "^2.8.0",
"@astrojs/internal-helpers": "0.4.0",
@ -10171,6 +10205,52 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/astro-i18next": {
"version": "1.0.0-beta.21",
"resolved": "https://registry.npmjs.org/astro-i18next/-/astro-i18next-1.0.0-beta.21.tgz",
"integrity": "sha512-1YPqwexumHpK/d9afEoi52CBFTu6k4MYv/oHjsaAasZDvFClU6U5VPttC/OgZcXRYggCM6ee2LOnyHqlmXOeLA==",
"license": "MIT",
"dependencies": {
"@proload/core": "^0.3.3",
"@proload/plugin-tsm": "^0.2.1",
"i18next": "^22.4.10",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-fs-backend": "^2.1.1",
"i18next-http-backend": "^2.1.1",
"iso-639-1": "^2.1.15",
"locale-emoji": "^0.3.0",
"pathe": "^1.1.0"
},
"bin": {
"astro-i18next": "dist/cli/index.js"
},
"peerDependencies": {
"astro": ">=1.0.0"
}
},
"node_modules/astro-i18next/node_modules/i18next": {
"version": "22.5.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz",
"integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.6"
}
},
"node_modules/astro/node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
@ -11028,6 +11108,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
@ -13508,6 +13589,7 @@
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dev": true,
"peer": true,
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
@ -13550,6 +13632,15 @@
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -14730,6 +14821,7 @@
"integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==",
"dev": true,
"hasInstallScript": true,
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -14761,6 +14853,262 @@
"@esbuild/win32-x64": "0.19.8"
}
},
"node_modules/esbuild-android-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-android-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-darwin-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-freebsd-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-mips64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-ppc64le": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-riscv64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-linux-s390x": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-netbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-openbsd-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-plugin-alias": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/esbuild-plugin-alias/-/esbuild-plugin-alias-0.2.1.tgz",
@ -14779,6 +15127,70 @@
"esbuild": ">=0.12 <1"
}
},
"node_modules/esbuild-sunos-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-32": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/esbuild-windows-arm64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -14827,6 +15239,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.50.0.tgz",
"integrity": "sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@ -15012,6 +15425,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz",
"integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==",
"dev": true,
"peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.findlastindex": "^1.2.2",
@ -17280,6 +17694,7 @@
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@ -17911,6 +18326,15 @@
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/html-tags": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@ -17995,6 +18419,62 @@
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/i18next": {
"version": "25.6.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.2.tgz",
"integrity": "sha512-0GawNyVUw0yvJoOEBq1VHMAsqdM23XrHkMtl2gKEjviJQSLVXsrPqsoYAxBEugW5AB96I2pZkwRxyl8WZVoWdw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.2.tgz",
"integrity": "sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-fs-backend": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.6.1.tgz",
"integrity": "sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==",
"license": "MIT"
},
"node_modules/i18next-http-backend": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.7.3.tgz",
"integrity": "sha512-FgZxrXdRA5u44xfYsJlEBL4/KH3f2IluBpgV/7riW0YW2VEyM8FzVt2XHAOi6id0Ppj7vZvCZVpp5LrGXnc8Ig==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -19063,6 +19543,15 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/iso-639-1": {
"version": "2.1.15",
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-2.1.15.tgz",
"integrity": "sha512-7c7mBznZu2ktfvyT582E2msM+Udc1EjOyhVRE/0ZsjD9LBtWSm23h3PtiRh2a35XoUsTQQjJXaJzuLjXsOdFDg==",
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
@ -19774,6 +20263,12 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/locale-emoji": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/locale-emoji/-/locale-emoji-0.3.0.tgz",
"integrity": "sha512-JGm8+naU49CBDnH1jksS3LecPdfWQLxFgkLN6ZhYONKa850pJ0Xt8DPGJnYK0ZuJI8jTuiDDPCDtSL3nyacXwg==",
"license": "CC0-1.0"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -21315,7 +21810,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
@ -21953,8 +22447,7 @@
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
"dev": true
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
},
"node_modules/pathval": {
"version": "1.1.1",
@ -22175,6 +22668,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
@ -22258,6 +22752,7 @@
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
"integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
"dev": true,
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -22312,6 +22807,7 @@
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.3.tgz",
"integrity": "sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==",
"dev": true,
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -22463,6 +22959,7 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -22633,6 +23130,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@ -22705,6 +23203,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@ -22746,6 +23245,42 @@
"react-dom": ">=16.8.1"
}
},
"node_modules/react-i18next": {
"version": "16.3.3",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.3.3.tgz",
"integrity": "sha512-IaY2W+ueVd/fe7H6Wj2S4bTuLNChnajFUlZFfCTrTHWzGcOrUHlVzW55oXRSl+J51U8Onn6EvIhQ+Bar9FUcjw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"i18next": ">= 25.6.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-i18next/node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-icons": {
"version": "4.11.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
@ -23011,11 +23546,6 @@
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/regenerator-transform": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
@ -23795,6 +24325,7 @@
"version": "4.18.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
"integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
"peer": true,
"dependencies": {
"@types/estree": "1.0.5"
},
@ -25076,6 +25607,7 @@
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-15.10.3.tgz",
"integrity": "sha512-aBQMMxYvFzJJwkmg+BUUg3YfPyeuCuKo2f+LOw7yYbU8AZMblibwzp9OV4srHVeQldxvSFdz0/Xu8blq2AesiA==",
"dev": true,
"peer": true,
"dependencies": {
"@csstools/css-parser-algorithms": "^2.3.1",
"@csstools/css-tokenizer": "^2.2.0",
@ -25675,6 +26207,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -25761,8 +26294,7 @@
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/trim-lines": {
"version": "3.0.1",
@ -25820,6 +26352,7 @@
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
"integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==",
"dev": true,
"peer": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@ -25930,6 +26463,90 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tsm": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tsm/-/tsm-2.3.0.tgz",
"integrity": "sha512-++0HFnmmR+gMpDtKTnW3XJ4yv9kVGi20n+NfyQWB9qwJvTaIWY9kBmzek2YUQK5APTQ/1DTrXmm4QtFPmW9Rzw==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.15.16"
},
"bin": {
"tsm": "bin.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/tsm/node_modules/@esbuild/android-arm": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsm/node_modules/@esbuild/linux-loong64": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsm/node_modules/esbuild": {
"version": "0.15.18",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.15.18",
"@esbuild/linux-loong64": "0.15.18",
"esbuild-android-64": "0.15.18",
"esbuild-android-arm64": "0.15.18",
"esbuild-darwin-64": "0.15.18",
"esbuild-darwin-arm64": "0.15.18",
"esbuild-freebsd-64": "0.15.18",
"esbuild-freebsd-arm64": "0.15.18",
"esbuild-linux-32": "0.15.18",
"esbuild-linux-64": "0.15.18",
"esbuild-linux-arm": "0.15.18",
"esbuild-linux-arm64": "0.15.18",
"esbuild-linux-mips64le": "0.15.18",
"esbuild-linux-ppc64le": "0.15.18",
"esbuild-linux-riscv64": "0.15.18",
"esbuild-linux-s390x": "0.15.18",
"esbuild-netbsd-64": "0.15.18",
"esbuild-openbsd-64": "0.15.18",
"esbuild-sunos-64": "0.15.18",
"esbuild-windows-32": "0.15.18",
"esbuild-windows-64": "0.15.18",
"esbuild-windows-arm64": "0.15.18"
}
},
"node_modules/tsutils": {
"version": "3.21.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
@ -26074,6 +26691,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"devOptional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -26571,6 +27189,7 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
"integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.38",
@ -27081,6 +27700,7 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true,
"peer": true,
"dependencies": {
"@vitest/expect": "1.6.0",
"@vitest/runner": "1.6.0",
@ -27155,6 +27775,15 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
@ -27189,8 +27818,7 @@
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/webpack-sources": {
"version": "3.2.3",
@ -27211,7 +27839,6 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
@ -27539,6 +28166,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -27615,6 +28243,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@ -28145,6 +28774,7 @@
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -36,15 +36,18 @@
"@types/react-dom": "^18.2.10",
"@vite-pwa/astro": "0.5.0",
"astro": "4.10.3",
"astro-i18next": "1.0.0-beta.21",
"deepmerge": "4.3.1",
"focus-trap-react": "10.2.3",
"framer-motion": "10.16.4",
"howler": "2.2.4",
"i18next": "25.6.2",
"js-confetti": "0.12.0",
"motion": "12.23.24",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "3.2.1",
"react-i18next": "16.3.3",
"react-icons": "4.11.0",
"react-wrap-balancer": "1.1.0",
"react-youtube": "10.1.0",

View file

@ -0,0 +1,119 @@
---
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
import { getTranslation } from '@/data/i18n';
// Get language from URL path
const url = Astro.url;
const pathname = url.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
const lang = isZhPage ? 'zh-CN' : 'en';
const t = getTranslation(lang);
const count = soundCount();
---
<section class="about">
<div class="effect"></div>
<Container tight>
</Container>
</section>
<style>
.about {
padding-top: 10px;
& .effect {
position: sticky;
top: 0;
height: 80px;
background: linear-gradient(var(--color-neutral-50), transparent);
}
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);
&:last-of-type {
padding-bottom: 0;
}
& .counter {
width: max-content;
padding: 6px 16px;
margin-bottom: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 20px 20px 20px 8px;
& span {
font-weight: 500;
color: var(--color-foreground);
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
margin-top: 20px;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
outline: none;
transition: 0.2s;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
&:hover,
&:focus-visible {
background-color: var(--color-neutral-100);
}
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
}
</style>

View file

@ -0,0 +1,173 @@
---
import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
---
<section class="about">
<div class="effect"></div>
<Container tight>
<div class="paragraph">
<div class="counter">
<span>01</span> / <span>04</span>
</div>
<h2 class="title">免费环境音</h2>
<p class="body">
渴望从日常繁杂中获得片刻宁静?需要完美的声音环境来提升专注力或帮助入眠?
Moodist 就是您的最佳选择——免费开源的环境音生成器!无需订阅注册,使用 Moodist
您可以免费享受舒缓沉浸的音频体验。
</p>
</div>
<div class="paragraph">
<div class="counter">
<span>02</span> / <span>04</span>
</div>
<h2 class="title">精心挑选的声音</h2>
<p class="body">
探索包含 <span class="sound-count">{count}</span> 个精心挑选声音的庞大音库。
自然爱好者可以在溪流的轻柔潺潺声中、海浪的节拍拍岸声中、或篝火的温暖噼啪声中获得慰藉。
城市景观在咖啡馆的轻柔嗡嗡声、火车的节拍咔嗒声、或交通的舒缓白噪声中变得生动。
对于寻求更深专注或放松的人Moodist 提供了专门设计来增强心境的双节拍和色彩噪声。
</p>
</div>
<div class="paragraph">
<div class="counter">
<span>03</span> / <span>04</span>
</div>
<h2 class="title">创造您的声音景观</h2>
<p class="body">
Moodist 的美妙之处在于其简洁性和自定义性。没有复杂的菜单或令人困惑的选项——只需选择您喜欢的声音,
调整音量平衡,然后点击播放。想要将鸟儿的轻柔啾鸣与雨水的舒缓声音融合?没问题!
随心所欲地叠加多个声音,创建个性化的声音绿洲。
</p>
</div>
<div class="paragraph">
<div class="counter">
<span>04</span> / <span>04</span>
</div>
<h2 class="title">适合每个时刻的声音</h2>
<p class="body">
无论您是想在漫长一天后放松身心,在工作中提升专注力,还是让自己进入宁静的睡眠,
Moodist 都有完美的声音景观等着您。最棒的是什么?它完全免费开源,您可以毫无负担地享受它的好处。
今天就开始使用 Moodist发现您新的宁静和专注天堂吧
</p>
</div>
<button class="button" id="use-moodist">使用 Moodist</button>
</Container>
</section>
<script lang="ts">
const button = document.getElementById('use-moodist');
button?.addEventListener('click', () => {
const app = document.getElementById('app');
app?.scrollIntoView();
});
</script>
<style>
.about {
padding-top: 10px;
& .effect {
position: sticky;
top: 0;
height: 80px;
background: linear-gradient(var(--color-neutral-50), transparent);
}
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);
&:last-of-type {
padding-bottom: 0;
}
& .counter {
width: max-content;
padding: 6px 16px;
margin-bottom: 16px;
font-size: var(--font-xsm);
color: var(--color-foreground-subtle);
background: linear-gradient(var(--color-neutral-100), transparent);
border: 1px solid var(--color-neutral-300);
border-radius: 20px 20px 20px 8px;
& span {
font-weight: 500;
color: var(--color-foreground);
}
}
& .title {
margin-bottom: 8px;
font-family: var(--font-heading);
font-size: var(--font-md);
font-weight: 600;
}
& .body {
line-height: 1.6;
color: var(--color-foreground-subtle);
}
}
.button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 16px;
margin-top: 20px;
font-size: var(--font-xsm);
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background-color: transparent;
border: 1px solid var(--color-neutral-200);
border-radius: 50px;
outline: none;
transition: 0.2s;
&::before {
position: absolute;
top: -1px;
left: 50%;
width: 70%;
height: 1px;
content: '';
background: linear-gradient(
90deg,
transparent,
var(--color-neutral-300),
transparent
);
transform: translateX(-50%);
}
&:hover,
&:focus-visible {
background-color: var(--color-neutral-100);
}
&:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
}
}
</style>

View file

@ -4,45 +4,53 @@ import { Container } from '@/components/container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
const paragraphs = [
{
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
title: 'Free Ambient Sounds',
},
{
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
title: 'Carefully Curated Sounds',
},
{
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
title: 'Create Your Soundscape',
},
{
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
title: 'Sounds for Every Moment',
},
];
---
<section class="about">
<div class="effect"></div>
<Container tight>
{
paragraphs.map((paragraph, index) => (
<div class="paragraph">
<div class="counter">
<span>0{index + 1}</span> / 0{paragraphs.length}
<span>01</span> / <span>04</span>
</div>
<h2 class="title" data-i18n="about.freeAmbientSounds.title">Free Ambient Sounds</h2>
<p class="body" data-i18n="about.freeAmbientSounds.body">
Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.
</p>
</div>
<h2 class="title">{paragraph.title}</h2>
<p class="body">{paragraph.body}</p>
<div class="paragraph">
<div class="counter">
<span>02</span> / <span>04</span>
</div>
<h2 class="title" data-i18n="about.carefullyCuratedSounds.title">Carefully Curated Sounds</h2>
<p class="body" data-i18n-count={count} data-i18n="about.carefullyCuratedSounds.body">
Dive into an expansive library of <span class="sound-count">{count}</span> carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.
</p>
</div>
))
}
<button class="button" id="use-moodist"> Use Moodist</button>
<div class="paragraph">
<div class="counter">
<span>03</span> / <span>04</span>
</div>
<h2 class="title" data-i18n="about.createYourSoundscape.title">Create Your Soundscape</h2>
<p class="body" data-i18n="about.createYourSoundscape.body">
The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.
</p>
</div>
<div class="paragraph">
<div class="counter">
<span>04</span> / <span>04</span>
</div>
<h2 class="title" data-i18n="about.soundsForEveryMoment.title">Sounds for Every Moment</h2>
<p class="body" data-i18n="about.soundsForEveryMoment.body">
Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!
</p>
</div>
<button class="button" id="use-moodist" data-i18n="about.useMoodist">Use Moodist</button>
</Container>
</section>

View file

@ -4,6 +4,8 @@ import { BiSolidHeart } from 'react-icons/bi/index';
import { Howler } from 'howler';
import { useSoundStore } from '@/stores/sound';
import { useLocalizedSounds } from '@/hooks/useLocalizedSounds';
import { useTranslation } from '@/hooks/useTranslation';
import { Container } from '@/components/container';
import { StoreConsumer } from '@/components/store-consumer';
@ -21,15 +23,17 @@ import type { Sound } from '@/data/types';
import { subscribe } from '@/lib/event';
export function App() {
const categories = useMemo(() => sounds.categories, []);
const localizedCategories = useLocalizedSounds();
const { t } = useTranslation();
const categories = useMemo(() => sounds.categories, []);
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
const pause = useSoundStore(state => state.pause);
const lock = useSoundStore(state => state.lock);
const unlock = useSoundStore(state => state.unlock);
const favoriteSounds = useMemo(() => {
const favoriteSounds = categories
const favoriteSounds = localizedCategories
.map(category => category.sounds)
.flat()
.filter(sound => favorites.includes(sound.id));
@ -40,7 +44,7 @@ export function App() {
return favorites.map(favorite =>
favoriteSounds.find(sound => sound.id === favorite),
);
}, [favorites, categories]);
}, [favorites, localizedCategories]);
useEffect(() => {
const onChange = () => {
@ -79,12 +83,12 @@ export function App() {
icon: <BiSolidHeart />,
id: 'favorites',
sounds: favoriteSounds as Array<Sound>,
title: 'Favorites',
title: t('favorite'),
});
}
return [...favorites, ...categories];
}, [favoriteSounds, categories]);
return [...favorites, ...localizedCategories];
}, [favoriteSounds, localizedCategories, t]);
return (
<SnackbarProvider>

View file

@ -4,11 +4,13 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { useSoundStore } from '@/stores/sound';
import { useSnackbar } from '@/contexts/snackbar';
import { useTranslation } from '@/hooks/useTranslation';
import { cn } from '@/helpers/styles';
import styles from './play.module.css';
export function PlayButton() {
const { t } = useTranslation();
const isPlaying = useSoundStore(state => state.isPlaying);
const pause = useSoundStore(state => state.pause);
const toggle = useSoundStore(state => state.togglePlay);
@ -42,14 +44,14 @@ export function PlayButton() {
<span aria-hidden="true">
<BiPause />
</span>{' '}
Pause
{t('pause')}
</>
) : (
<>
<span aria-hidden="true">
<BiPlay />
</span>{' '}
Play
{t('play')}
</>
)}
</button>

View file

@ -1,7 +1,6 @@
import { AnimatePresence } from 'motion/react';
import { Category } from './category';
import { Donate } from './donate';
import type { Categories } from '@/data/types';
@ -12,12 +11,8 @@ interface CategoriesProps {
export function Categories({ categories }: CategoriesProps) {
return (
<AnimatePresence initial={false}>
{categories.map((category, index) => (
<div key={category.id}>
<Category functional={category.id !== 'favorites'} {...category} />
{index === 3 && <Donate />}
</div>
{categories.map((category) => (
<Category key={category.id} functional={category.id !== 'favorites'} {...category} />
))}
</AnimatePresence>
);

View file

@ -1,10 +1,13 @@
import { FaCoffee } from 'react-icons/fa/index';
import { SpecialButton } from '@/components/special-button';
import { useTranslation } from '@/hooks/useTranslation';
import styles from './donate.module.css';
export function Donate() {
const { t } = useTranslation();
return (
<div className={styles.donate}>
<div className={styles.iconContainer}>
@ -15,14 +18,14 @@ export function Donate() {
</div>
<div className={styles.title}>
<span>Support Me</span>
<span>{t('supportMe')}</span>
</div>
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
<p className={styles.desc}>{t('helpKeepAdFree')}</p>
<SpecialButton
className={styles.button}
href="https://buymeacoffee.com/remvze"
>
Donate Today
{t('donateToday')}
</SpecialButton>
</div>
);

View file

@ -1,17 +1,25 @@
---
import { Container } from './container';
import { getTranslation } from '@/data/i18n';
// Get language from URL path
const url = Astro.url;
const pathname = url.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
const lang = isZhPage ? 'zh-CN' : 'en';
const t = getTranslation(lang);
---
<Container>
<section class="wrapper">
<p class="text">
Enjoy Moodist?{' '}
{t.enjoyMoodist}{' '}
<a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
{t.supportWithDonation}
</a>
</p>
</section>

View file

@ -1,11 +1,19 @@
---
import { Container } from './container';
import { getTranslation } from '@/data/i18n';
// Get language from URL path
const url = Astro.url;
const pathname = url.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
const lang = isZhPage ? 'zh-CN' : 'en';
const t = getTranslation(lang);
---
<footer class="footer">
<Container>
<p>
Created by <a href="https://twitter.com/remvze">Maze ✦</a>
{t.createdBy} <a href="https://twitter.com/remvze">Maze ✦</a>
</p>
</Container>
</footer>

View file

@ -2,9 +2,15 @@
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from './container';
import { getTranslation } from '@/data/i18n';
import { count as soundCount } from '@/lib/sounds';
// Get language from URL path
const url = Astro.url;
const pathname = url.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
const lang = isZhPage ? 'zh-CN' : 'en';
const t = getTranslation(lang);
const count = soundCount();
---
@ -24,15 +30,15 @@ const count = soundCount();
</div>
<h1 class="title">
Ambient Sounds<span class="line">For Focus and Calm</span>
{t.heroTitle}<span class="line">{t.heroSubtitle}</span>
</h1>
<h2 class="desc">Free and Open-Source.</h2>
<h2 class="desc">{t.heroDescription}</h2>
<p class="sounds">
<span aria-hidden="true" class="icon">
<BsSoundwave />
</span>
<span>{count} Sounds</span>
<span>{count}{t.soundsCount}</span>
</p>
</div>
</Container>

View file

@ -0,0 +1 @@
export * from './language-switcher';

View file

@ -0,0 +1,43 @@
.languageSwitcher {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border: 1px solid var(--color-neutral-200);
border-radius: 8px;
background-color: var(--color-neutral-50);
color: var(--color-foreground);
font-size: var(--font-xsm);
transition: all 0.2s ease;
}
.icon {
color: var(--color-foreground-subtle);
font-size: 14px;
}
.select {
background: transparent;
border: none;
color: var(--color-foreground);
font-size: var(--font-xsm);
cursor: pointer;
outline: none;
padding: 2px;
border-radius: 4px;
min-width: 80px;
}
.select:hover {
background-color: var(--color-neutral-100);
}
.select:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
.languageSwitcher:hover {
background-color: var(--color-neutral-100);
border-color: var(--color-neutral-300);
}

View file

@ -0,0 +1,31 @@
import { FaGlobe } from 'react-icons/fa/index';
import { useTranslation } from '@/hooks/useTranslation';
import styles from './language-switcher.module.css';
interface LanguageSwitcherProps {
className?: string;
}
export function LanguageSwitcher({ className }: LanguageSwitcherProps) {
const { currentLang, changeLanguage, t } = useTranslation();
const handleLanguageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
changeLanguage(e.target.value);
};
return (
<div className={`${styles.languageSwitcher} ${className || ''}`}>
<FaGlobe className={styles.icon} />
<select
value={currentLang}
onChange={handleLanguageChange}
className={styles.select}
aria-label={t('app.language') || 'Select language'}
>
<option value="en">English</option>
<option value="zh-CN"></option>
</select>
</div>
);
}

View file

@ -51,14 +51,16 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
return (
<Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Share your sound selection!</h1>
<p className={styles.desc}>
<h1 className={styles.heading} data-i18n="modals.share.title">
Share your sound selection!
</h1>
<p className={styles.desc} data-i18n="modals.share.description">
Copy and send the following link to the person you want to share your
selection with.
</p>
<div className={styles.inputWrapper}>
<input readOnly type="text" value={url} />
<button onClick={() => copy(url)}>
<button onClick={() => copy(url)} aria-label="Copy link">
{copying ? <IoCheckmark /> : <IoCopyOutline />}
</button>
</div>

View file

@ -1,4 +1,5 @@
import { useSoundStore } from '@/stores/sound';
import { useTranslation } from '@/hooks/useTranslation';
import styles from './range.module.css';
@ -8,6 +9,7 @@ interface RangeProps {
}
export function Range({ id, label }: RangeProps) {
const { t } = useTranslation();
const setVolume = useSoundStore(state => state.setVolume);
const volume = useSoundStore(state => state.sounds[id].volume);
const isSelected = useSoundStore(state => state.sounds[id].isSelected);
@ -15,7 +17,7 @@ export function Range({ id, label }: RangeProps) {
return (
<input
aria-label={`${label} sound volume`}
aria-label={`${label} ${t('volume').toLowerCase()}`}
autoComplete="off"
className={styles.range}
disabled={!isSelected}

View file

@ -3,6 +3,7 @@ import { AnimatePresence, motion } from 'motion/react';
import { Sound } from './sound';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { useTranslation } from '@/hooks/useTranslation';
import { cn } from '@/helpers/styles';
import { fade, scale, mix } from '@/lib/motion';
@ -17,6 +18,7 @@ interface SoundsProps {
}
export function Sounds({ functional, id, sounds }: SoundsProps) {
const { t } = useTranslation();
const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false);
const [clickedMore, setClickedMore] = useState(false);
@ -106,7 +108,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
onAnimationComplete={() => setIsAnimating(false)}
onAnimationStart={() => setIsAnimating(true)}
>
{showAll ? 'Show Less' : 'Show More'}
{showAll ? t('showLess') : t('showMore')}
</motion.span>
</AnimatePresence>
</button>

View file

@ -5,6 +5,7 @@ import styles from './item.module.css';
interface ItemProps {
active?: boolean;
'data-i18n'?: string;
disabled?: boolean;
href?: string;
icon: React.ReactElement;
@ -15,6 +16,7 @@ interface ItemProps {
export function Item({
active,
'data-i18n': dataI18n,
disabled = false,
href,
icon,
@ -30,10 +32,11 @@ export function Item({
className={styles.item}
disabled={disabled}
{...(href ? { href, target: '_blank' } : {})}
{...(dataI18n ? { 'data-i18n': dataI18n } : {})}
aria-label={label}
>
<span className={styles.label}>
<span className={styles.icon}>{icon}</span> {label}
<span className={styles.icon}>{icon}</span> <span data-i18n={dataI18n}>{label}</span>
{active && <div className={styles.active} />}
</span>

View file

@ -1,13 +1,16 @@
import { FaHeadphonesAlt } from 'react-icons/fa/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface BinauralProps {
open: () => void;
}
export function Binaural({ open }: BinauralProps) {
const { t } = useTranslation();
return (
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
<Item icon={<FaHeadphonesAlt />} label={t('binauralBeats')} onClick={open} />
);
}

View file

@ -1,16 +1,19 @@
import { IoMdFlower } from 'react-icons/io/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface BreathingExerciseProps {
open: () => void;
}
export function BreathingExercise({ open }: BreathingExerciseProps) {
const { t } = useTranslation();
return (
<Item
icon={<IoMdFlower />}
label="Breathing Exercise"
label={t('breathingExercise')}
shortcut="Shift + B"
onClick={open}
/>

View file

@ -1,16 +1,19 @@
import { MdOutlineTimer } from 'react-icons/md/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface CountdownProps {
open: () => void;
}
export function Countdown({ open }: CountdownProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdOutlineTimer />}
label="Countdown Timer"
label={t('countdownTimer')}
shortcut="Shift + C"
onClick={open}
/>

View file

@ -1,13 +1,16 @@
import { SiBuymeacoffee } from 'react-icons/si/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
export function Donate() {
const { t } = useTranslation();
return (
<Item
href="https://buymeacoffee.com/remvze"
icon={<SiBuymeacoffee />}
label="Buy Me a Coffee"
label={t('buyMeACoffee')}
/>
);
}

View file

@ -1,11 +1,14 @@
import { TbWaveSine } from 'react-icons/tb/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface IsochronicProps {
open: () => void;
}
export function Isochronic({ open }: IsochronicProps) {
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
const { t } = useTranslation();
return <Item icon={<TbWaveSine />} label={t('isochronicTones')} onClick={open} />;
}

View file

@ -1,16 +1,19 @@
import { IoIosMusicalNote } from 'react-icons/io/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface LofiProps {
open: () => void;
}
export function Lofi({ open }: LofiProps) {
const { t } = useTranslation();
return (
<Item
icon={<IoIosMusicalNote />}
label="Lofi Music Player"
label={t('lofiMusicPlayer')}
onClick={open}
/>
);

View file

@ -3,19 +3,21 @@ import { MdNotes } from 'react-icons/md/index';
import { Item } from '../item';
import { useNoteStore } from '@/stores/note';
import { useTranslation } from '@/hooks/useTranslation';
interface NotepadProps {
open: () => void;
}
export function Notepad({ open }: NotepadProps) {
const { t } = useTranslation();
const note = useNoteStore(state => state.note);
return (
<Item
active={!!note.length}
icon={<MdNotes />}
label="Notepad"
label={t('notepad')}
shortcut="Shift + N"
onClick={open}
/>

View file

@ -3,19 +3,21 @@ import { MdOutlineAvTimer } from 'react-icons/md/index';
import { Item } from '../item';
import { usePomodoroStore } from '@/stores/pomodoro';
import { useTranslation } from '@/hooks/useTranslation';
interface PomodoroProps {
open: () => void;
}
export function Pomodoro({ open }: PomodoroProps) {
const { t } = useTranslation();
const running = usePomodoroStore(state => state.running);
return (
<Item
active={running}
icon={<MdOutlineAvTimer />}
label="Pomodoro"
label={t('pomodoro')}
shortcut="Shift + P"
onClick={open}
/>

View file

@ -1,16 +1,20 @@
import { RiPlayListFill } from 'react-icons/ri/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface PresetsProps {
open: () => void;
}
export function Presets({ open }: PresetsProps) {
const { t } = useTranslation();
return (
<Item
icon={<RiPlayListFill />}
label="Your Presets"
label={t('presets')}
data-i18n="navigation.presets"
shortcut="Shift + Alt + P"
onClick={open}
/>

View file

@ -3,19 +3,21 @@ import { IoShareSocialSharp } from 'react-icons/io5/index';
import { Item } from '../item';
import { useSoundStore } from '@/stores/sound';
import { useTranslation } from '@/hooks/useTranslation';
interface ShareProps {
open: () => void;
}
export function Share({ open }: ShareProps) {
const { t } = useTranslation();
const noSelected = useSoundStore(state => state.noSelected());
return (
<Item
disabled={noSelected}
icon={<IoShareSocialSharp />}
label="Share Sounds"
label={t('share')}
shortcut="Shift + S"
onClick={open}
/>

View file

@ -1,16 +1,19 @@
import { MdKeyboardCommandKey } from 'react-icons/md/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface ShortcutsProps {
open: () => void;
}
export function Shortcuts({ open }: ShortcutsProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdKeyboardCommandKey />}
label="Shortcuts"
label={t('shortcuts')}
shortcut="Shift + H"
onClick={open}
/>

View file

@ -3,8 +3,10 @@ import { BiShuffle } from 'react-icons/bi/index';
import { useSoundStore } from '@/stores/sound';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
export function Shuffle() {
const { t } = useTranslation();
const shuffle = useSoundStore(state => state.shuffle);
const locked = useSoundStore(state => state.locked);
@ -12,7 +14,7 @@ export function Shuffle() {
<Item
disabled={locked}
icon={<BiShuffle />}
label="Shuffle Sounds"
label={t('shuffleSounds')}
onClick={shuffle}
/>
);

View file

@ -2,19 +2,21 @@ import { IoMoonSharp } from 'react-icons/io5/index';
import { useSleepTimerStore } from '@/stores/sleep-timer';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface SleepTimerProps {
open: () => void;
}
export function SleepTimer({ open }: SleepTimerProps) {
const { t } = useTranslation();
const active = useSleepTimerStore(state => state.active);
return (
<Item
active={active}
icon={<IoMoonSharp />}
label="Sleep Timer"
label={t('sleepTimer')}
shortcut="Shift + Alt + T"
onClick={open}
/>

View file

@ -1,13 +1,16 @@
import { LuGithub } from 'react-icons/lu/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
export function Source() {
const { t } = useTranslation();
return (
<Item
href="https://github.com/remvze/moodist"
icon={<LuGithub />}
label="Source Code"
label={t('sourceCode')}
/>
);
}

View file

@ -1,16 +1,19 @@
import { MdTaskAlt } from 'react-icons/md/index';
import { Item } from '../item';
import { useTranslation } from '@/hooks/useTranslation';
interface TodoProps {
open: () => void;
}
export function Todo({ open }: TodoProps) {
const { t } = useTranslation();
return (
<Item
icon={<MdTaskAlt />}
label="Todo Checklist"
label={t('todoChecklist')}
shortcut="Shift + T"
onClick={open}
/>

View file

@ -32,6 +32,7 @@ import { IsochronicModal } from '@/components/modals/isochronic';
import { LofiModal } from '@/components/modals/lofi';
import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox';
import { Slider } from '@/components/slider';
import { useTranslation } from '@/hooks/useTranslation';
import { fade, mix, slideY } from '@/lib/motion';
import { useSoundStore } from '@/stores/sound';
@ -41,6 +42,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
import { closeModals } from '@/lib/modal';
export function Menu() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const noSelected = useSoundStore(state => state.noSelected());
@ -103,7 +105,7 @@ export function Menu() {
<div className={styles.wrapper}>
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
<DropdownMenu.Trigger asChild>
<button aria-label="Menu" className={styles.menuButton}>
<button aria-label={t('menu')} className={styles.menuButton}>
{isOpen ? <IoClose /> : <IoMenu />}
</button>
</DropdownMenu.Trigger>
@ -147,7 +149,7 @@ export function Menu() {
<Divider />
<div className={styles.globalVolume}>
<label htmlFor="global-volume">Global Volume</label>
<label htmlFor="global-volume">{t('globalVolume')}</label>
<Slider
max={100}
min={0}

522
src/data/i18n.ts Normal file
View file

@ -0,0 +1,522 @@
export interface Translations {
// Navigation & UI
presets: string;
share: string;
useMoodist: string;
// Hero section
heroTitle: string;
heroSubtitle: string;
heroDescription: string;
soundsCount: string;
// About section
freeAmbientSounds: {
title: string;
body: string;
};
carefullyCuratedSounds: {
title: string;
body: string;
};
createYourSoundscape: {
title: string;
body: string;
};
soundsForEveryMoment: {
title: string;
body: string;
};
// Categories
categories: Record<string, string>;
// Sounds
sounds: Record<string, Record<string, string>>;
// Common
play: string;
pause: string;
favorite: string;
volume: string;
// Support & Donate
supportMe: string;
helpKeepAdFree: string;
donateToday: string;
buyMeACoffee: string;
createdBy: string;
enjoyMoodist: string;
supportWithDonation: string;
// UI Actions
showMore: string;
showLess: string;
// Settings
globalVolume: string;
menu: string;
// Menu Items
breathingExercise: string;
countdownTimer: string;
sleepTimer: string;
pomodoro: string;
notepad: string;
todoChecklist: string;
lofiMusicPlayer: string;
binauralBeats: string;
isochronicTones: string;
shortcuts: string;
shuffleSounds: string;
sourceCode: string;
}
export const translations: Record<string, Translations> = {
en: {
// Navigation & UI
presets: 'Your Presets',
share: 'Share',
useMoodist: 'Use Moodist',
// Hero section
heroTitle: 'Ambient Sounds',
heroSubtitle: 'For Focus and Calm',
heroDescription: 'Free and Open-Source.',
soundsCount: 'Sounds',
// About section
freeAmbientSounds: {
title: 'Free Ambient Sounds',
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.'
},
carefullyCuratedSounds: {
title: 'Carefully Curated Sounds',
body: 'Dive into an expansive library of {{count}} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.'
},
createYourSoundscape: {
title: 'Create Your Soundscape',
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.'
},
soundsForEveryMoment: {
title: 'Sounds for Every Moment',
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!"
},
// Categories
categories: {
nature: 'Nature',
rain: 'Rain',
animals: 'Animals',
urban: 'Urban',
places: 'Places',
transport: 'Transport',
things: 'Things',
noise: 'Noise'
},
// Sounds
sounds: {
nature: {
river: 'River',
waves: 'Waves',
campfire: 'Campfire',
wind: 'Wind',
howlingWind: 'Howling Wind',
windInTrees: 'Wind in Trees',
waterfall: 'Waterfall',
walkInSnow: 'Walk in Snow',
walkOnLeaves: 'Walk on Leaves',
walkOnGravel: 'Walk on Gravel',
droplets: 'Droplets',
jungle: 'Jungle'
},
rain: {
lightRain: 'Light Rain',
moderateRain: 'Moderate Rain',
heavyRain: 'Heavy Rain',
storm: 'Storm',
thunder: 'Thunder',
distantStorm: 'Distant Storm',
fire: 'Fire',
oceanWaves: 'Ocean Waves',
rainOnLeaves: 'Rain on Leaves',
rainOnPavement: 'Rain on Pavement',
rainOnWindow: 'Rain on Window',
rainOnUmbrella: 'Rain on Umbrella',
rainOnTent: 'Rain on Tent',
insideRain: 'Inside the Rain',
carRain: 'Car Rain'
},
animals: {
birds: 'Birds',
seagulls: 'Seagulls',
crickets: 'Crickets',
wolves: 'Wolves',
owl: 'Owl',
frogs: 'Frogs',
dogs: 'Dogs',
horses: 'Horses',
cats: 'Cats',
crows: 'Crows',
whale: 'Whale',
beehive: 'Beehive',
woodpecker: 'Woodpecker',
chickens: 'Chickens',
cows: 'Cows',
sheep: 'Sheep',
rooster: 'Rooster',
birdsMorning: 'Birds in Morning',
birdsEvening: 'Birds in Evening'
},
urban: {
cityStreet: 'City Street',
traffic: 'Traffic',
highway: 'Highway',
road: 'Road',
ambulanceSiren: 'Ambulance Siren',
busyStreet: 'Busy Street',
crowd: 'Crowd',
fireworks: 'Fireworks'
},
places: {
forest: 'Forest',
beach: 'Beach',
park: 'Park',
mountain: 'Mountain',
desert: 'Desert',
cave: 'Cave',
meadow: 'Meadow',
lake: 'Lake',
campsite: 'Campsite',
temple: 'Temple',
airport: 'Airport',
church: 'Church',
underwater: 'Underwater',
crowdedBar: 'Crowded Bar',
nightVillage: 'Night Village',
carousel: 'Carousel',
laboratory: 'Laboratory',
laundryRoom: 'Laundry Room',
subwayStation: 'Subway Station',
cafe: 'Cafe',
constructionSite: 'Construction Site',
office: 'Office',
supermarket: 'Supermarket',
restaurant: 'Restaurant',
library: 'Library'
},
transport: {
car: 'Car',
bus: 'Bus',
train: 'Train',
subway: 'Subway',
airplane: 'Airplane',
boat: 'Boat',
bicycle: 'Bicycle',
motorcycle: 'Motorcycle',
helicopter: 'Helicopter',
steamTrain: 'Steam Train',
insideTrain: 'Inside a Train',
submarine: 'Submarine',
sailboat: 'Sailboat',
rowingBoat: 'Rowing Boat'
},
things: {
fan: 'Fan',
clock: 'Clock',
typewriter: 'Typewriter',
keyboard: 'Keyboard',
printer: 'Printer',
refrigerator: 'Refrigerator',
washingMachine: 'Washing Machine',
vacuum: 'Vacuum',
airConditioner: 'Air Conditioner',
microwave: 'Microwave',
paper: 'Paper',
windChimes: 'Wind Chimes',
singingBowl: 'Singing Bowl',
ceilingFan: 'Ceiling Fan',
dryer: 'Dryer',
slideProjector: 'Slide Projector',
boilingWater: 'Boiling Water',
bubbles: 'Bubbles',
tuningRadio: 'Tuning Radio',
morseCode: 'Morse Code',
vinylEffect: 'Vinyl Effect',
windshieldWipers: 'Windshield Wipers'
},
noise: {
whiteNoise: 'White Noise',
pinkNoise: 'Pink Noise',
brownNoise: 'Brown Noise',
blueNoise: 'Blue Noise',
violetNoise: 'Violet Noise',
greyNoise: 'Grey Noise'
}
},
// Common
play: 'Play',
pause: 'Pause',
favorite: 'Favorite',
volume: 'Volume',
// Support & Donate
supportMe: 'Support Me',
helpKeepAdFree: 'Help me keep Moodist ad-free.',
donateToday: 'Donate Today',
buyMeACoffee: 'Buy Me a Coffee',
createdBy: 'Created by',
enjoyMoodist: 'Enjoy Moodist?',
supportWithDonation: 'Support with a donation!',
// UI Actions
showMore: 'Show More',
showLess: 'Show Less',
// Settings
globalVolume: 'Global Volume',
menu: 'Menu',
// Menu Items
breathingExercise: 'Breathing Exercise',
countdownTimer: 'Countdown Timer',
sleepTimer: 'Sleep Timer',
pomodoro: 'Pomodoro',
notepad: 'Notepad',
todoChecklist: 'Todo Checklist',
lofiMusicPlayer: 'Lofi Music Player',
binauralBeats: 'Binaural Beats',
isochronicTones: 'Isochronic Tones',
shortcuts: 'Shortcuts',
shuffleSounds: 'Shuffle Sounds',
sourceCode: 'Source Code'
},
'zh-CN': {
// Navigation & UI
app: { language: '语言' },
presets: '我的预设',
share: '分享',
useMoodist: '使用 Moodist',
// Hero section
heroTitle: '环境音',
heroSubtitle: '专注与宁静',
heroDescription: '免费开源。',
soundsCount: '个声音',
// About section
freeAmbientSounds: {
title: '免费环境音',
body: '渴望从日常繁杂中获得片刻宁静需要完美的声音环境来提升专注力或帮助入眠Moodist 就是您的最佳选择——免费开源的环境音生成器!无需订阅注册,使用 Moodist您可以免费享受舒缓沉浸的音频体验。'
},
carefullyCuratedSounds: {
title: '精心挑选的声音',
body: '探索包含 {{count}} 个精心挑选声音的庞大音库。自然爱好者可以在溪流的轻柔潺潺声中、海浪的节拍拍岸声中、或篝火的温暖噼啪声中获得慰藉。城市景观在咖啡馆的轻柔嗡嗡声、火车的节拍咔嗒声、或交通的舒缓白噪声中变得生动。对于寻求更深专注或放松的人Moodist 提供了专门设计来增强心境的双节拍和色彩噪声。'
},
createYourSoundscape: {
title: '创造您的声音景观',
body: 'Moodist 的美妙之处在于其简洁性和自定义性。没有复杂的菜单或令人困惑的选项——只需选择您喜欢的声音,调整音量平衡,然后点击播放。想要将鸟儿的轻柔啾鸣与雨水的舒缓声音融合?没问题!随心所欲地叠加多个声音,创建个性化的声音绿洲。'
},
soundsForEveryMoment: {
title: '适合每个时刻的声音',
body: '无论您是想在漫长一天后放松身心在工作中提升专注力还是让自己进入宁静的睡眠Moodist 都有完美的声音景观等着您。最棒的是什么?它完全免费开源,您可以毫无负担地享受它的好处。今天就开始使用 Moodist发现您新的宁静和专注天堂吧'
},
// Categories
categories: {
nature: '自然',
rain: '雨声',
animals: '动物',
urban: '城市',
places: '地点',
transport: '交通',
things: '物品',
noise: '噪声'
},
// Sounds
sounds: {
nature: {
river: '河流',
waves: '波浪',
campfire: '篝火',
wind: '风声',
howlingWind: '呼啸的风',
windInTrees: '林中风声',
waterfall: '瀑布',
walkInSnow: '雪地漫步',
walkOnLeaves: '落叶漫步',
walkOnGravel: '碎石漫步',
droplets: '水滴',
jungle: '丛林'
},
rain: {
lightRain: '小雨',
moderateRain: '中雨',
heavyRain: '大雨',
storm: '暴风雨',
thunder: '雷声',
distantStorm: '远处的暴风雨',
fire: '火焰',
oceanWaves: '海浪',
rainOnLeaves: '雨打叶子',
rainOnPavement: '雨打路面',
rainOnWindow: '雨打窗户',
rainOnUmbrella: '雨打雨伞',
rainOnTent: '雨打帐篷',
insideRain: '室内雨声',
carRain: '车中雨声'
},
animals: {
birds: '鸟儿',
seagulls: '海鸥',
crickets: '蟋蟀',
wolves: '狼群',
owl: '猫头鹰',
frogs: '青蛙',
dogs: '狗狗',
horses: '马儿',
cats: '猫咪',
crows: '乌鸦',
whale: '鲸鱼',
beehive: '蜂箱',
woodpecker: '啄木鸟',
chickens: '小鸡',
cows: '奶牛',
sheep: '绵羊',
rooster: '公鸡',
birdsMorning: '清晨鸟鸣',
birdsEvening: '傍晚鸟鸣'
},
urban: {
cityStreet: '城市街道',
traffic: '交通',
highway: '高速公路',
road: '马路',
ambulanceSiren: '救护车警报',
busyStreet: '繁忙街道',
crowd: '人群',
fireworks: '烟花'
},
places: {
forest: '森林',
beach: '海滩',
park: '公园',
mountain: '山脉',
desert: '沙漠',
cave: '洞穴',
meadow: '草原',
lake: '湖泊',
campsite: '露营地',
temple: '寺庙',
airport: '机场',
church: '教堂',
underwater: '水下',
crowdedBar: '拥挤酒吧',
nightVillage: '夜晚村庄',
carousel: '旋转木马',
laboratory: '实验室',
laundryRoom: '洗衣房',
subwayStation: '地铁站',
cafe: '咖啡馆',
constructionSite: '施工现场',
office: '办公室',
supermarket: '超市',
restaurant: '餐厅',
library: '图书馆'
},
transport: {
car: '汽车',
bus: '公交车',
train: '火车',
subway: '地铁',
airplane: '飞机',
boat: '小船',
bicycle: '自行车',
motorcycle: '摩托车',
helicopter: '直升机',
steamTrain: '蒸汽火车',
insideTrain: '火车内部',
submarine: '潜水艇',
sailboat: '帆船',
rowingBoat: '划船'
},
things: {
fan: '风扇',
clock: '时钟',
typewriter: '打字机',
keyboard: '键盘',
printer: '打印机',
refrigerator: '冰箱',
washingMachine: '洗衣机',
vacuum: '吸尘器',
airConditioner: '空调',
microwave: '微波炉',
paper: '纸张',
windChimes: '风铃',
singingBowl: '颂钵',
ceilingFan: '吊扇',
dryer: '干衣机',
slideProjector: '幻灯机',
boilingWater: '沸腾的水',
bubbles: '泡泡',
tuningRadio: '调谐收音机',
morseCode: '摩斯电码',
vinylEffect: '黑胶唱片效果',
windshieldWipers: '雨刮器'
},
noise: {
whiteNoise: '白噪声',
pinkNoise: '粉噪声',
brownNoise: '棕噪声',
blueNoise: '蓝噪声',
violetNoise: '紫噪声',
greyNoise: '灰噪声'
}
},
// Common
play: '播放',
pause: '暂停',
favorite: '收藏',
volume: '音量',
// Support & Donate
supportMe: '支持我',
helpKeepAdFree: '帮助我保持 Moodist 无广告。',
donateToday: '立即捐赠',
buyMeACoffee: '请我喝杯咖啡',
createdBy: '作者',
enjoyMoodist: '喜欢 Moodist 吗?',
supportWithDonation: '欢迎捐赠支持!',
// UI Actions
showMore: '更多',
showLess: '收起',
// Settings
globalVolume: '全局音量',
menu: '菜单',
// Menu Items
breathingExercise: '呼吸练习',
countdownTimer: '倒计时器',
sleepTimer: '睡眠定时器',
pomodoro: '番茄钟',
notepad: '记事本',
todoChecklist: '待办清单',
lofiMusicPlayer: 'Lofi 音乐播放器',
binauralBeats: '双节拍',
isochronicTones: '等时音',
shortcuts: '快捷键',
shuffleSounds: '随机播放',
sourceCode: '源代码'
}
};
export function getTranslation(lang: string = 'en'): Translations {
return translations[lang] || translations.en;
}

View file

@ -31,98 +31,115 @@ export const animals: Category = {
icon: <PiBirdFill />,
id: 'birds',
label: 'Birds',
dataI18n: 'sounds.animals.birds',
src: getAssetPath('/sounds/animals/birds.mp3'),
},
{
icon: <GiSeagull />,
id: 'seagulls',
label: 'Seagulls',
dataI18n: 'sounds.animals.seagulls',
src: getAssetPath('/sounds/animals/seagulls.mp3'),
},
{
icon: <GiCricket />,
id: 'crickets',
label: 'Crickets',
dataI18n: 'sounds.animals.crickets',
src: getAssetPath('/sounds/animals/crickets.mp3'),
},
{
icon: <GiWolfHead />,
id: 'wolf',
label: 'Wolf',
dataI18n: 'sounds.animals.wolves',
src: getAssetPath('/sounds/animals/wolf.mp3'),
},
{
icon: <GiOwl />,
id: 'owl',
label: 'Owl',
dataI18n: 'sounds.animals.owl',
src: getAssetPath('/sounds/animals/owl.mp3'),
},
{
icon: <FaFrog />,
id: 'frog',
label: 'Frog',
dataI18n: 'sounds.animals.frogs',
src: getAssetPath('/sounds/animals/frog.mp3'),
},
{
icon: <PiDogBold />,
id: 'dog-barking',
label: 'Dog Barking',
dataI18n: 'sounds.animals.dogs',
src: getAssetPath('/sounds/animals/dog-barking.mp3'),
},
{
icon: <FaHorseHead />,
id: 'horse-gallop',
label: 'Horse Gallop',
dataI18n: 'sounds.animals.horses',
src: getAssetPath('/sounds/animals/horse-gallop.mp3'),
},
{
icon: <FaCat />,
id: 'cat-purring',
label: 'Cat Purring',
dataI18n: 'sounds.animals.cats',
src: getAssetPath('/sounds/animals/cat-purring.mp3'),
},
{
icon: <FaCrow />,
id: 'crows',
label: 'Crows',
dataI18n: 'sounds.animals.crows',
src: getAssetPath('/sounds/animals/crows.mp3'),
},
{
icon: <GiWhaleTail />,
id: 'whale',
label: 'Whale',
dataI18n: 'sounds.animals.whale',
src: getAssetPath('/sounds/animals/whale.mp3'),
},
{
icon: <GiTreeBeehive />,
id: 'beehive',
label: 'Beehive',
dataI18n: 'sounds.animals.beehive',
src: getAssetPath('/sounds/animals/beehive.mp3'),
},
{
icon: <GiEgyptianBird />,
id: 'woodpecker',
label: 'Woodpecker',
dataI18n: 'sounds.animals.woodpecker',
src: getAssetPath('/sounds/animals/woodpecker.mp3'),
},
{
icon: <GiChicken />,
id: 'chickens',
label: 'Chickens',
dataI18n: 'sounds.animals.chickens',
src: getAssetPath('/sounds/animals/chickens.mp3'),
},
{
icon: <GiCow />,
id: 'cows',
label: 'Cows',
dataI18n: 'sounds.animals.cows',
src: getAssetPath('/sounds/animals/cows.mp3'),
},
{
icon: <GiSheep />,
id: 'sheep',
label: 'Sheep',
dataI18n: 'sounds.animals.sheep',
src: getAssetPath('/sounds/animals/sheep.mp3'),
},
],
title: 'Animals',
dataI18n: 'categories.animals',
};

View file

@ -21,74 +21,87 @@ export const nature: Category = {
icon: <BiWater />,
id: 'river',
label: 'River',
dataI18n: 'sounds.nature.river',
src: getAssetPath('/sounds/nature/river.mp3'),
},
{
icon: <FaWater />,
id: 'waves',
label: 'Waves',
dataI18n: 'sounds.nature.waves',
src: getAssetPath('/sounds/nature/waves.mp3'),
},
{
icon: <BsFire />,
id: 'campfire',
label: 'Campfire',
dataI18n: 'sounds.nature.campfire',
src: getAssetPath('/sounds/nature/campfire.mp3'),
},
{
icon: <FaWind />,
id: 'wind',
label: 'Wind',
dataI18n: 'sounds.nature.wind',
src: getAssetPath('/sounds/nature/wind.mp3'),
},
{
icon: <FaWind />,
id: 'howling-wind',
label: 'Howling Wind',
dataI18n: 'sounds.nature.howlingWind',
src: getAssetPath('/sounds/nature/howling-wind.mp3'),
},
{
icon: <BiSolidTree />,
id: 'wind-in-trees',
label: 'Wind in Trees',
dataI18n: 'sounds.nature.windInTrees',
src: getAssetPath('/sounds/nature/wind-in-trees.mp3'),
},
{
icon: <GiWaterfall />,
id: 'waterfall',
label: 'Waterfall',
dataI18n: 'sounds.nature.waterfall',
src: getAssetPath('/sounds/nature/waterfall.mp3'),
},
{
icon: <FaRegSnowflake />,
id: 'walk-in-snow',
label: 'Walk in Snow',
dataI18n: 'sounds.nature.walkInSnow',
src: getAssetPath('/sounds/nature/walk-in-snow.mp3'),
},
{
icon: <FaLeaf />,
id: 'walk-on-leaves',
label: 'Walk on Leaves',
dataI18n: 'sounds.nature.walkOnLeaves',
src: getAssetPath('/sounds/nature/walk-on-leaves.mp3'),
},
{
icon: <GiStonePile />,
id: 'walk-on-gravel',
label: 'Walk on Gravel',
dataI18n: 'sounds.nature.walkOnGravel',
src: getAssetPath('/sounds/nature/walk-on-gravel.mp3'),
},
{
icon: <BsFillDropletFill />,
id: 'droplets',
label: 'Droplets',
dataI18n: 'sounds.nature.droplets',
src: getAssetPath('/sounds/nature/droplets.mp3'),
},
{
icon: <FaTree />,
id: 'jungle',
label: 'Jungle',
dataI18n: 'sounds.nature.jungle',
src: getAssetPath('/sounds/nature/jungle.mp3'),
},
],
title: 'Nature',
dataI18n: 'categories.nature',
};

View file

@ -13,20 +13,24 @@ export const noise: Category = {
icon: <GiSoundWaves />,
id: 'white-noise',
label: 'White Noise',
dataI18n: 'sounds.noise.whiteNoise',
src: getAssetPath('/sounds/noise/white-noise.wav'),
},
{
icon: <GiSoundWaves />,
id: 'pink-noise',
label: 'Pink Noise',
dataI18n: 'sounds.noise.pinkNoise',
src: getAssetPath('/sounds/noise/pink-noise.wav'),
},
{
icon: <GiSoundWaves />,
id: 'brown-noise',
label: 'Brown Noise',
dataI18n: 'sounds.noise.brownNoise',
src: getAssetPath('/sounds/noise/brown-noise.wav'),
},
],
title: 'Noise',
dataI18n: 'categories.noise',
};

View file

@ -28,98 +28,115 @@ export const places: Category = {
icon: <BiSolidCoffeeAlt />,
id: 'cafe',
label: 'Cafe',
dataI18n: 'sounds.places.cafe',
src: getAssetPath('/sounds/places/cafe.mp3'),
},
{
icon: <BiSolidPlaneAlt />,
id: 'airport',
label: 'Airport',
dataI18n: 'sounds.places.airport',
src: getAssetPath('/sounds/places/airport.mp3'),
},
{
icon: <FaChurch />,
id: 'church',
label: 'Church',
dataI18n: 'sounds.places.church',
src: getAssetPath('/sounds/places/church.mp3'),
},
{
icon: <MdTempleBuddhist />,
id: 'temple',
label: 'Temple',
dataI18n: 'sounds.places.temple',
src: getAssetPath('/sounds/places/temple.mp3'),
},
{
icon: <MdConstruction />,
id: 'construction-site',
label: 'Construction Site',
dataI18n: 'sounds.places.constructionSite',
src: getAssetPath('/sounds/places/construction-site.mp3'),
},
{
icon: <TbScubaMask />,
id: 'underwater',
label: 'Underwater',
dataI18n: 'sounds.places.underwater',
src: getAssetPath('/sounds/places/underwater.mp3'),
},
{
icon: <TbBeerFilled />,
id: 'crowded-bar',
label: 'Crowded Bar',
dataI18n: 'sounds.places.crowdedBar',
src: getAssetPath('/sounds/places/crowded-bar.mp3'),
},
{
icon: <GiVillage />,
id: 'night-village',
label: 'Night Village',
dataI18n: 'sounds.places.nightVillage',
src: getAssetPath('/sounds/places/night-village.mp3'),
},
{
icon: <FaSubway />,
id: 'subway-station',
label: 'Subway Station',
dataI18n: 'sounds.places.subwayStation',
src: getAssetPath('/sounds/places/subway-station.mp3'),
},
{
icon: <HiOfficeBuilding />,
id: 'office',
label: 'Office',
dataI18n: 'sounds.places.office',
src: getAssetPath('/sounds/places/office.mp3'),
},
{
icon: <FaShoppingBasket />,
id: 'supermarket',
label: 'Supermarket',
dataI18n: 'sounds.places.supermarket',
src: getAssetPath('/sounds/places/supermarket.mp3'),
},
{
icon: <GiCarousel />,
id: 'carousel',
label: 'Carousel',
dataI18n: 'sounds.places.carousel',
src: getAssetPath('/sounds/places/carousel.mp3'),
},
{
icon: <AiFillExperiment />,
id: 'laboratory',
label: 'Laboratory',
dataI18n: 'sounds.places.laboratory',
src: getAssetPath('/sounds/places/laboratory.mp3'),
},
{
icon: <BiSolidDryer />,
id: 'laundry-room',
label: 'Laundry Room',
dataI18n: 'sounds.places.laundryRoom',
src: getAssetPath('/sounds/places/laundry-room.mp3'),
},
{
icon: <IoRestaurant />,
id: 'restaurant',
label: 'Restaurant',
dataI18n: 'sounds.places.restaurant',
src: getAssetPath('/sounds/places/restaurant.mp3'),
},
{
icon: <FaBookOpen />,
id: 'library',
label: 'Library',
dataI18n: 'sounds.places.library',
src: getAssetPath('/sounds/places/library.mp3'),
},
],
title: 'Places',
dataI18n: 'categories.places',
};

View file

@ -20,50 +20,73 @@ export const rain: Category = {
icon: <BsFillCloudRainFill />,
id: 'light-rain',
label: 'Light Rain',
dataI18n: 'sounds.rain.lightRain',
src: getAssetPath('/sounds/rain/light-rain.mp3'),
},
{
icon: <BsFillCloudRainHeavyFill />,
id: 'moderate-rain',
label: 'Moderate Rain',
dataI18n: 'sounds.rain.moderateRain',
src: getAssetPath('/sounds/rain/moderate-rain.mp3'),
},
{
icon: <BsFillCloudRainHeavyFill />,
id: 'heavy-rain',
label: 'Heavy Rain',
dataI18n: 'sounds.rain.heavyRain',
src: getAssetPath('/sounds/rain/heavy-rain.mp3'),
},
{
icon: <MdOutlineThunderstorm />,
id: 'storm',
label: 'Storm',
dataI18n: 'sounds.rain.storm',
src: getAssetPath('/sounds/rain/storm.mp3'),
},
{
icon: <MdOutlineThunderstorm />,
id: 'thunder',
label: 'Thunder',
dataI18n: 'sounds.rain.thunder',
src: getAssetPath('/sounds/rain/thunder.mp3'),
},
{
icon: <GiWindow />,
id: 'rain-on-window',
label: 'Rain on Window',
dataI18n: 'sounds.rain.rainOnWindow',
src: getAssetPath('/sounds/rain/rain-on-window.mp3'),
},
{
icon: <FaCarSide />,
id: 'rain-on-car-roof',
label: 'Rain on Car Roof',
dataI18n: 'sounds.rain.carRain',
src: getAssetPath('/sounds/rain/rain-on-car-roof.mp3'),
},
{
icon: <BsUmbrellaFill />,
id: 'rain-on-umbrella',
label: 'Rain on Umbrella',
dataI18n: 'sounds.rain.rainOnUmbrella',
src: getAssetPath('/sounds/rain/rain-on-umbrella.mp3'),
},
{
icon: <PiTentFill />,
id: 'rain-on-tent',
label: 'Rain on Tent',
dataI18n: 'sounds.rain.rainOnTent',
src: getAssetPath('/sounds/rain/rain-on-tent.mp3'),
},
{
icon: <FaLeaf />,
id: 'rain-on-leaves',
label: 'Rain on Leaves',
dataI18n: 'sounds.rain.rainOnLeaves',
src: getAssetPath('/sounds/rain/rain-on-leaves.mp3'),
},
],
title: 'Rain',
dataI18n: 'categories.rain',
};

View file

@ -24,98 +24,115 @@ export const things: Category = {
icon: <BsFillKeyboardFill />,
id: 'keyboard',
label: 'Keyboard',
dataI18n: 'sounds.things.keyboard',
src: getAssetPath('/sounds/things/keyboard.mp3'),
},
{
icon: <FaKeyboard />,
id: 'typewriter',
label: 'Typewriter',
dataI18n: 'sounds.things.typewriter',
src: getAssetPath('/sounds/things/typewriter.mp3'),
},
{
icon: <RiFilePaper2Fill />,
id: 'paper',
label: 'Paper',
dataI18n: 'sounds.things.paper',
src: getAssetPath('/sounds/things/paper.mp3'),
},
{
icon: <FaClock />,
id: 'clock',
label: 'Clock',
dataI18n: 'sounds.things.clock',
src: getAssetPath('/sounds/things/clock.mp3'),
},
{
icon: <GiWindchimes />,
id: 'wind-chimes',
label: 'Wind Chimes',
dataI18n: 'sounds.things.windChimes',
src: getAssetPath('/sounds/things/wind-chimes.mp3'),
},
{
icon: <TbBowlFilled />,
id: 'singing-bowl',
label: 'Singing Bowl',
dataI18n: 'sounds.things.singingBowl',
src: getAssetPath('/sounds/things/singing-bowl.mp3'),
},
{
icon: <FaFan />,
id: 'ceiling-fan',
label: 'Ceiling Fan',
dataI18n: 'sounds.things.ceilingFan',
src: getAssetPath('/sounds/things/ceiling-fan.mp3'),
},
{
icon: <BiSolidDryer />,
id: 'dryer',
label: 'Dryer',
dataI18n: 'sounds.things.dryer',
src: getAssetPath('/sounds/things/dryer.mp3'),
},
{
icon: <GiFilmProjector />,
id: 'slide-projector',
label: 'Slide Projector',
dataI18n: 'sounds.things.slideProjector',
src: getAssetPath('/sounds/things/slide-projector.mp3'),
},
{
icon: <MdWaterDrop />,
id: 'boiling-water',
label: 'Boiling Water',
dataI18n: 'sounds.things.boilingWater',
src: getAssetPath('/sounds/things/boiling-water.mp3'),
},
{
icon: <RiBubbleChartFill />,
id: 'bubbles',
label: 'Bubbles',
dataI18n: 'sounds.things.bubbles',
src: getAssetPath('/sounds/things/bubbles.mp3'),
},
{
icon: <MdRadio />,
id: 'tuning-radio',
label: 'Tuning Radio',
dataI18n: 'sounds.things.tuningRadio',
src: getAssetPath('/sounds/things/tuning-radio.mp3'),
},
{
icon: <IoIosRadio />,
id: 'morse-code',
label: 'Morse Code',
dataI18n: 'sounds.things.morseCode',
src: getAssetPath('/sounds/things/morse-code.mp3'),
},
{
icon: <GiWashingMachine />,
id: 'washing-machine',
label: 'Washing Machine',
dataI18n: 'sounds.things.washingMachine',
src: getAssetPath('/sounds/things/washing-machine.mp3'),
},
{
icon: <PiVinylRecord />,
id: 'vinyl-effect',
label: 'Vinyl Effect',
dataI18n: 'sounds.things.vinylEffect',
src: getAssetPath('/sounds/things/vinyl-effect.mp3'),
},
{
icon: <TbWiper />,
id: 'windshield-wipers',
label: 'Windshield Wipers',
dataI18n: 'sounds.things.windshieldWipers',
src: getAssetPath('/sounds/things/windshield-wipers.mp3'),
},
],
title: 'Things',
dataI18n: 'categories.things',
};

View file

@ -15,38 +15,45 @@ export const transport: Category = {
icon: <BiSolidTrain />,
id: 'train',
label: 'Train',
dataI18n: 'sounds.transport.train',
src: getAssetPath('/sounds/transport/train.mp3'),
},
{
icon: <BiSolidTrain />,
id: 'inside-a-train',
label: 'Inside a Train',
dataI18n: 'sounds.transport.insideTrain',
src: getAssetPath('/sounds/transport/inside-a-train.mp3'),
},
{
icon: <BiSolidPlaneAlt />,
id: 'airplane',
label: 'Airplane',
dataI18n: 'sounds.transport.airplane',
src: getAssetPath('/sounds/transport/airplane.mp3'),
},
{
icon: <GiSubmarine />,
id: 'submarine',
label: 'Submarine',
dataI18n: 'sounds.transport.submarine',
src: getAssetPath('/sounds/transport/submarine.mp3'),
},
{
icon: <GiSailboat />,
id: 'sailboat',
label: 'Sailboat',
dataI18n: 'sounds.transport.sailboat',
src: getAssetPath('/sounds/transport/sailboat.mp3'),
},
{
icon: <TbSailboat />,
id: 'rowing-boat',
label: 'Rowing Boat',
dataI18n: 'sounds.transport.rowingBoat',
src: getAssetPath('/sounds/transport/rowing-boat.mp3'),
},
],
title: 'Transport',
dataI18n: 'categories.transport',
};

View file

@ -16,44 +16,52 @@ export const urban: Category = {
icon: <PiRoadHorizonFill />,
id: 'highway',
label: 'Highway',
dataI18n: 'sounds.urban.highway',
src: getAssetPath('/sounds/urban/highway.mp3'),
},
{
icon: <FaRoad />,
id: 'road',
label: 'Road',
dataI18n: 'sounds.urban.road',
src: getAssetPath('/sounds/urban/road.mp3'),
},
{
icon: <PiSirenBold />,
id: 'ambulance-siren',
label: 'Ambulance Siren',
dataI18n: 'sounds.urban.ambulanceSiren',
src: getAssetPath('/sounds/urban/ambulance-siren.mp3'),
},
{
icon: <BsSoundwave />,
id: 'busy-street',
label: 'Busy Street',
dataI18n: 'sounds.urban.busyStreet',
src: getAssetPath('/sounds/urban/busy-street.mp3'),
},
{
icon: <BsPeopleFill />,
id: 'crowd',
label: 'Crowd',
dataI18n: 'sounds.urban.crowd',
src: getAssetPath('/sounds/urban/crowd.mp3'),
},
{
icon: <BiSolidTraffic />,
id: 'traffic',
label: 'Traffic',
dataI18n: 'sounds.urban.traffic',
src: getAssetPath('/sounds/urban/traffic.mp3'),
},
{
icon: <RiSparkling2Fill />,
id: 'fireworks',
label: 'Fireworks',
dataI18n: 'sounds.urban.fireworks',
src: getAssetPath('/sounds/urban/fireworks.mp3'),
},
],
title: 'Urban',
dataI18n: 'categories.urban',
};

2
src/data/types.d.ts vendored
View file

@ -3,6 +3,7 @@ export interface Sound {
id: string;
label: string;
src: string;
dataI18n?: string;
}
export type Sounds = Array<Sound>;
@ -12,6 +13,7 @@ export interface Category {
id: string;
sounds: Sounds;
title: string;
dataI18n?: string;
}
export type Categories = Array<Category>;

View file

@ -0,0 +1,18 @@
import { useTranslation } from '@/hooks/use-translation';
export function useTranslatedSounds() {
const { t } = useTranslation();
const translateCategory = (category: string) => {
return t(`categories.${category.toLowerCase()}`);
};
const translateSound = (category: string, soundId: string) => {
return t(`sounds.${category.toLowerCase()}.${soundId}`);
};
return {
translateCategory,
translateSound,
};
}

43
src/hooks/useLanguage.ts Normal file
View file

@ -0,0 +1,43 @@
import { useState, useEffect } from 'react';
export function useLanguage() {
const [currentLang, setCurrentLang] = useState('en');
useEffect(() => {
// Detect language from URL path first
const pathname = window.location.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
// Then check URL parameter
const urlParams = new URLSearchParams(window.location.search);
const urlLang = urlParams.get('lang');
// Finally check saved preference
const savedLang = localStorage.getItem('moodist-language');
const lang = urlLang || (isZhPage ? 'zh-CN' : savedLang) || 'en';
setCurrentLang(lang);
}, []);
const changeLanguage = (lang: string) => {
setCurrentLang(lang);
localStorage.setItem('moodist-language', lang);
// Update URL and navigate if needed
const url = new URL(window.location.href);
if (lang === 'zh-CN') {
// Navigate to Chinese page
window.location.href = `${window.location.origin}/zh`;
} else {
// Navigate to English page
window.location.href = `${window.location.origin}/`;
}
};
return {
currentLang,
changeLanguage,
isChinese: currentLang === 'zh-CN'
};
}

View file

@ -0,0 +1,35 @@
import { useMemo } from 'react';
import { sounds } from '@/data/sounds';
import type { Category, Sound } from '@/data/types';
import { useTranslation } from './useTranslation';
export function useLocalizedSounds() {
const { t } = useTranslation();
const localizedSounds = useMemo(() => {
return sounds.categories.map((category: Category): Category => {
// Translate category title
const categoryKey = `categories.${category.id}`;
const translatedTitle = t(categoryKey as any);
// Translate sounds
const translatedSounds = category.sounds.map((sound: Sound): Sound => {
if (sound.dataI18n) {
return {
...sound,
label: t(sound.dataI18n as any) || sound.label
};
}
return sound;
});
return {
...category,
title: translatedTitle,
sounds: translatedSounds
};
});
}, [t]);
return localizedSounds;
}

View file

@ -0,0 +1,43 @@
import { useLanguage } from './useLanguage';
import { getTranslation } from '@/data/i18n';
import type { Translations } from '@/data/i18n';
export function useTranslation() {
const { currentLang, isChinese, changeLanguage } = useLanguage();
const translations: Translations = getTranslation(currentLang);
const t = (key: keyof Translations, params?: Record<string, string | number>) => {
let translation = getNestedValue(translations, key) as string;
if (typeof translation !== 'string') {
// Fallback to English if translation is missing
const englishTranslations = getTranslation('en');
translation = getNestedValue(englishTranslations, key) as string;
}
if (typeof translation !== 'string') {
return key; // Return key if no translation found
}
// Replace parameters like {{count}}
if (params) {
return translation.replace(/\{\{(\w+)\}\}/g, (match, param) => {
return params[param]?.toString() || match;
});
}
return translation;
};
return {
t,
currentLang,
isChinese,
changeLanguage,
translations
};
}
function getNestedValue(obj: any, path: string) {
return path.split('.').reduce((current, key) => current?.[key], obj);
}

View file

@ -2,6 +2,7 @@
import { pwaInfo } from 'virtual:pwa-info'; // eslint-disable-line
import { Reload } from '@/components/reload';
import { LanguageSwitcher } from '@/components/language-switcher';
import { count } from '@/lib/sounds';
@ -16,10 +17,15 @@ const title = Astro.props.title || 'Moodist: Ambient Sounds for Focus and Calm';
const description =
Astro.props.description ||
`Moodist is a free and open-source ambient sound generator featuring ${count()} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.`;
// Get language from URL parameter
const url = Astro.url;
const langParam = url.searchParams.get('lang') || 'en';
const lang = ['en', 'zh-CN'].includes(langParam) ? langParam : 'en';
---
<!doctype html>
<html lang="en">
<html lang={lang}>
<head>
<meta charset="UTF-8" />
<meta content="width=device-width" name="viewport" />
@ -40,11 +46,36 @@ const description =
<meta content="summary_large_image" name="twitter:card" />
<!-- Hreflang tags for SEO -->
<link rel="alternate" hreflang="en" href="https://moodist.app" />
<link rel="alternate" hreflang="zh-CN" href="https://moodist.app?lang=zh-CN" />
<link rel="alternate" hreflang="x-default" href="https://moodist.app" />
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
</head>
<body>
<LanguageSwitcher client:load className="language-switcher-fixed" />
<slot />
<Reload client:load />
</body>
<style>
.language-switcher-fixed {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(24, 24, 27, 0.8);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
@media (max-width: 768px) {
.language-switcher-fixed {
top: 15px;
right: 15px;
}
}
</style>
</html>

View file

@ -3,18 +3,26 @@ import Layout from '@/layouts/layout.astro';
import Donate from '@/components/donate.astro';
import Hero from '@/components/hero.astro';
import About from '@/components/about.astro';
import Source from '@/components/source.astro';
import Footer from '@/components/footer.astro';
import About from '@/components/about-unified.astro';
import { App } from '@/components/app';
import { getTranslation } from '@/data/i18n';
// Get language from URL path
const url = Astro.url;
const pathname = url.pathname;
const isZhPage = pathname === '/zh' || pathname.startsWith('/zh/');
const lang = isZhPage ? 'zh-CN' : 'en';
const t = getTranslation(lang);
const pageTitle = lang === 'zh-CN' ? 'Moodist专注与放松的环境音' : 'Moodist: Ambient Sounds for Focus and Calm';
const pageDesc = lang === 'zh-CN'
? 'Moodist 是一个免费开源的环境音生成器,提供精心挑选的声音,帮您创造放松、专注或创意的理想氛围。'
: 'Moodist is a free and open-source ambient sound generator featuring carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.';
---
<Layout title="Moodist: Ambient Sounds for Focus and Calm">
<Layout title={pageTitle} description={pageDesc}>
<Donate />
<Hero />
<App client:load />
<About />
<Source />
<Footer />
</Layout>

21
src/pages/zh.astro Normal file
View file

@ -0,0 +1,21 @@
---
import Layout from '@/layouts/layout.astro';
import Donate from '@/components/donate.astro';
import Hero from '@/components/hero.astro';
import About from '@/components/about-unified.astro';
import { App } from '@/components/app';
import { getTranslation } from '@/data/i18n';
const t = getTranslation('zh-CN');
const pageTitle = 'Moodist专注与放松的环境音';
const pageDesc = 'Moodist 是一个免费开源的环境音生成器,提供精心挑选的声音,帮您创造放松、专注或创意的理想氛围。';
---
<Layout title={pageTitle} description={pageDesc}>
<Donate />
<Hero />
<App client:load />
<About />
</Layout>