From 65958f84827dc2fc02f74dbc2b8a6174e90c1586 Mon Sep 17 00:00:00 2001 From: zl Date: Sun, 16 Nov 2025 14:30:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=95=B4=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=B8=AD=E8=8B=B1=E6=96=87=E5=8F=8C=E8=AF=AD=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=89=80=E6=9C=89=E5=A3=B0=E9=9F=B3?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要功能 - 实现完整的中英文双语支持系统 - 添加语言切换器和路由配置 - 创建统一的翻译文件和hooks ## 核心组件 - 新增语言切换器组件 - 实现中英文页面路由 - 添加翻译系统核心文件 ## 翻译修复 - 修复所有声音名称的dataI18n映射 - 解决重复翻译键冲突问题 - 完善所有分类的声音翻译 ## 声音分类优化 - 修复雨声分类的重复翻译键问题 - 清理跨分类翻译键冲突 - 优化声音分类归属 ## UI优化 - 移除页面底部开源模块 - 完善顶部捐赠文本翻译 - 优化所有菜单项的翻译显示 --- .serena/.gitignore | 1 + .serena/project.yml | 84 +++ .spec-workflow/templates/design-template.md | 96 +++ .spec-workflow/templates/product-template.md | 51 ++ .../templates/requirements-template.md | 50 ++ .../templates/structure-template.md | 145 ++++ .spec-workflow/templates/tasks-template.md | 139 ++++ .spec-workflow/templates/tech-template.md | 99 +++ .spec-workflow/user-templates/README.md | 64 ++ astro.config.mjs | 5 + package-lock.json | 672 +++++++++++++++++- package.json | 3 + src/components/about-unified.astro | 119 ++++ src/components/about-zh.astro | 173 +++++ src/components/about.astro | 70 +- src/components/app/app.tsx | 16 +- src/components/buttons/play/play.tsx | 6 +- src/components/categories/categories.tsx | 9 +- src/components/categories/donate/donate.tsx | 9 +- src/components/donate.astro | 12 +- src/components/footer.astro | 10 +- src/components/hero.astro | 14 +- src/components/language-switcher/index.ts | 1 + .../language-switcher.module.css | 43 ++ .../language-switcher/language-switcher.tsx | 31 + .../modals/share-link/share-link.tsx | 8 +- src/components/sounds/sound/range/range.tsx | 4 +- src/components/sounds/sounds.tsx | 4 +- src/components/toolbar/menu/item/item.tsx | 5 +- .../toolbar/menu/items/binaural.tsx | 5 +- .../toolbar/menu/items/breathing-exercise.tsx | 5 +- .../toolbar/menu/items/countdown.tsx | 5 +- src/components/toolbar/menu/items/donate.tsx | 5 +- .../toolbar/menu/items/isochronic.tsx | 5 +- src/components/toolbar/menu/items/lofi.tsx | 5 +- src/components/toolbar/menu/items/notepad.tsx | 4 +- .../toolbar/menu/items/pomodoro.tsx | 4 +- src/components/toolbar/menu/items/presets.tsx | 6 +- src/components/toolbar/menu/items/share.tsx | 4 +- .../toolbar/menu/items/shortcuts.tsx | 5 +- src/components/toolbar/menu/items/shuffle.tsx | 4 +- .../toolbar/menu/items/sleep-timer.tsx | 4 +- src/components/toolbar/menu/items/source.tsx | 5 +- src/components/toolbar/menu/items/todo.tsx | 5 +- src/components/toolbar/menu/menu.tsx | 6 +- src/data/i18n.ts | 522 ++++++++++++++ src/data/sounds/animals.tsx | 17 + src/data/sounds/nature.tsx | 13 + src/data/sounds/noise.tsx | 4 + src/data/sounds/places.tsx | 17 + src/data/sounds/rain.tsx | 23 + src/data/sounds/things.tsx | 17 + src/data/sounds/transport.tsx | 7 + src/data/sounds/urban.tsx | 8 + src/data/types.d.ts | 2 + src/helpers/translation.ts | 18 + src/hooks/useLanguage.ts | 43 ++ src/hooks/useLocalizedSounds.ts | 35 + src/hooks/useTranslation.ts | 43 ++ src/layouts/layout.astro | 33 +- src/pages/index.astro | 22 +- src/pages/zh.astro | 21 + 62 files changed, 2757 insertions(+), 108 deletions(-) create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 .spec-workflow/templates/design-template.md create mode 100644 .spec-workflow/templates/product-template.md create mode 100644 .spec-workflow/templates/requirements-template.md create mode 100644 .spec-workflow/templates/structure-template.md create mode 100644 .spec-workflow/templates/tasks-template.md create mode 100644 .spec-workflow/templates/tech-template.md create mode 100644 .spec-workflow/user-templates/README.md create mode 100644 src/components/about-unified.astro create mode 100644 src/components/about-zh.astro create mode 100644 src/components/language-switcher/index.ts create mode 100644 src/components/language-switcher/language-switcher.module.css create mode 100644 src/components/language-switcher/language-switcher.tsx create mode 100644 src/data/i18n.ts create mode 100644 src/helpers/translation.ts create mode 100644 src/hooks/useLanguage.ts create mode 100644 src/hooks/useLocalizedSounds.ts create mode 100644 src/hooks/useTranslation.ts create mode 100644 src/pages/zh.astro diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..14d86ad --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..6e004bb --- /dev/null +++ b/.serena/project.yml @@ -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: [] diff --git a/.spec-workflow/templates/design-template.md b/.spec-workflow/templates/design-template.md new file mode 100644 index 0000000..54cd5d2 --- /dev/null +++ b/.spec-workflow/templates/design-template.md @@ -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] diff --git a/.spec-workflow/templates/product-template.md b/.spec-workflow/templates/product-template.md new file mode 100644 index 0000000..f806628 --- /dev/null +++ b/.spec-workflow/templates/product-template.md @@ -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] diff --git a/.spec-workflow/templates/requirements-template.md b/.spec-workflow/templates/requirements-template.md new file mode 100644 index 0000000..8db51e2 --- /dev/null +++ b/.spec-workflow/templates/requirements-template.md @@ -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] diff --git a/.spec-workflow/templates/structure-template.md b/.spec-workflow/templates/structure-template.md new file mode 100644 index 0000000..eb559be --- /dev/null +++ b/.spec-workflow/templates/structure-template.md @@ -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 diff --git a/.spec-workflow/templates/tasks-template.md b/.spec-workflow/templates/tasks-template.md new file mode 100644 index 0000000..5e494c0 --- /dev/null +++ b/.spec-workflow/templates/tasks-template.md @@ -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_ diff --git a/.spec-workflow/templates/tech-template.md b/.spec-workflow/templates/tech-template.md new file mode 100644 index 0000000..d98ee07 --- /dev/null +++ b/.spec-workflow/templates/tech-template.md @@ -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] diff --git a/.spec-workflow/user-templates/README.md b/.spec-workflow/user-templates/README.md new file mode 100644 index 0000000..ad36a48 --- /dev/null +++ b/.spec-workflow/user-templates/README.md @@ -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 diff --git a/astro.config.mjs b/astro.config.mjs index 82f6cc6..ebd6454 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -33,4 +33,9 @@ export default defineConfig({ }, }), ], + vite: { + define: { + global: 'globalThis', + }, + }, }); diff --git a/package-lock.json b/package-lock.json index a4d5785..e7906e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" } diff --git a/package.json b/package.json index a47e597..fbe6c63 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/about-unified.astro b/src/components/about-unified.astro new file mode 100644 index 0000000..0d029f5 --- /dev/null +++ b/src/components/about-unified.astro @@ -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(); +--- + +
+
+ + + +
+ + \ No newline at end of file diff --git a/src/components/about-zh.astro b/src/components/about-zh.astro new file mode 100644 index 0000000..c9fe004 --- /dev/null +++ b/src/components/about-zh.astro @@ -0,0 +1,173 @@ +--- +import { Container } from '@/components/container'; + +import { count as soundCount } from '@/lib/sounds'; + +const count = soundCount(); +--- + +
+
+ + +
+
+ 01 / 04 +
+

免费环境音

+

+ 渴望从日常繁杂中获得片刻宁静?需要完美的声音环境来提升专注力或帮助入眠? + Moodist 就是您的最佳选择——免费开源的环境音生成器!无需订阅注册,使用 Moodist, + 您可以免费享受舒缓沉浸的音频体验。 +

+
+ +
+
+ 02 / 04 +
+

精心挑选的声音

+

+ 探索包含 {count} 个精心挑选声音的庞大音库。 + 自然爱好者可以在溪流的轻柔潺潺声中、海浪的节拍拍岸声中、或篝火的温暖噼啪声中获得慰藉。 + 城市景观在咖啡馆的轻柔嗡嗡声、火车的节拍咔嗒声、或交通的舒缓白噪声中变得生动。 + 对于寻求更深专注或放松的人,Moodist 提供了专门设计来增强心境的双节拍和色彩噪声。 +

+
+ +
+
+ 03 / 04 +
+

创造您的声音景观

+

+ Moodist 的美妙之处在于其简洁性和自定义性。没有复杂的菜单或令人困惑的选项——只需选择您喜欢的声音, + 调整音量平衡,然后点击播放。想要将鸟儿的轻柔啾鸣与雨水的舒缓声音融合?没问题! + 随心所欲地叠加多个声音,创建个性化的声音绿洲。 +

+
+ +
+
+ 04 / 04 +
+

适合每个时刻的声音

+

+ 无论您是想在漫长一天后放松身心,在工作中提升专注力,还是让自己进入宁静的睡眠, + Moodist 都有完美的声音景观等着您。最棒的是什么?它完全免费开源,您可以毫无负担地享受它的好处。 + 今天就开始使用 Moodist,发现您新的宁静和专注天堂吧! +

+
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/components/about.astro b/src/components/about.astro index 72fd439..fdff981 100644 --- a/src/components/about.astro +++ b/src/components/about.astro @@ -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', - }, -]; ---
- { - paragraphs.map((paragraph, index) => ( -
-
- 0{index + 1} / 0{paragraphs.length} -
+
+
+ 01 / 04 +
+

Free Ambient Sounds

+

+ 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. +

+
-

{paragraph.title}

-

{paragraph.body}

-
- )) - } +
+
+ 02 / 04 +
+

Carefully Curated Sounds

+

+ 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. +

+
- +
+
+ 03 / 04 +
+

Create Your Soundscape

+

+ 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. +

+
+ +
+
+ 04 / 04 +
+

Sounds for Every Moment

+

+ 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! +

+
+ +
diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index c5f890d..656036c 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -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: , id: 'favorites', sounds: favoriteSounds as Array, - title: 'Favorites', + title: t('favorite'), }); } - return [...favorites, ...categories]; - }, [favoriteSounds, categories]); + return [...favorites, ...localizedCategories]; + }, [favoriteSounds, localizedCategories, t]); return ( diff --git a/src/components/buttons/play/play.tsx b/src/components/buttons/play/play.tsx index cd4afa9..c467af2 100644 --- a/src/components/buttons/play/play.tsx +++ b/src/components/buttons/play/play.tsx @@ -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() { {' '} - Pause + {t('pause')} ) : ( <> {' '} - Play + {t('play')} )} diff --git a/src/components/categories/categories.tsx b/src/components/categories/categories.tsx index 8e2f558..bd9fc0a 100644 --- a/src/components/categories/categories.tsx +++ b/src/components/categories/categories.tsx @@ -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 ( - {categories.map((category, index) => ( -
- - - {index === 3 && } -
+ {categories.map((category) => ( + ))}
); diff --git a/src/components/categories/donate/donate.tsx b/src/components/categories/donate/donate.tsx index 93a1774..67b5444 100644 --- a/src/components/categories/donate/donate.tsx +++ b/src/components/categories/donate/donate.tsx @@ -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 (
@@ -15,14 +18,14 @@ export function Donate() {
- Support Me + {t('supportMe')}
-

Help me keep Moodist ad-free.

+

{t('helpKeepAdFree')}

- Donate Today + {t('donateToday')}
); diff --git a/src/components/donate.astro b/src/components/donate.astro index 631a846..cac9a5a 100644 --- a/src/components/donate.astro +++ b/src/components/donate.astro @@ -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); ---

- Enjoy Moodist?{' '} + {t.enjoyMoodist}{' '} - Support with a donation! + {t.supportWithDonation}

diff --git a/src/components/footer.astro b/src/components/footer.astro index bfed23f..e93885a 100644 --- a/src/components/footer.astro +++ b/src/components/footer.astro @@ -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); --- diff --git a/src/components/hero.astro b/src/components/hero.astro index 5afe3e7..6f872d4 100644 --- a/src/components/hero.astro +++ b/src/components/hero.astro @@ -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();

- Ambient SoundsFor Focus and Calm + {t.heroTitle}{t.heroSubtitle}

-

Free and Open-Source.

+

{t.heroDescription}

- {count} Sounds + {count}{t.soundsCount}

diff --git a/src/components/language-switcher/index.ts b/src/components/language-switcher/index.ts new file mode 100644 index 0000000..faf1bf2 --- /dev/null +++ b/src/components/language-switcher/index.ts @@ -0,0 +1 @@ +export * from './language-switcher'; \ No newline at end of file diff --git a/src/components/language-switcher/language-switcher.module.css b/src/components/language-switcher/language-switcher.module.css new file mode 100644 index 0000000..06da8d1 --- /dev/null +++ b/src/components/language-switcher/language-switcher.module.css @@ -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); +} \ No newline at end of file diff --git a/src/components/language-switcher/language-switcher.tsx b/src/components/language-switcher/language-switcher.tsx new file mode 100644 index 0000000..641e35a --- /dev/null +++ b/src/components/language-switcher/language-switcher.tsx @@ -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) => { + changeLanguage(e.target.value); + }; + + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/src/components/modals/share-link/share-link.tsx b/src/components/modals/share-link/share-link.tsx index 8a75960..76b0e0c 100644 --- a/src/components/modals/share-link/share-link.tsx +++ b/src/components/modals/share-link/share-link.tsx @@ -51,14 +51,16 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) { return ( -

Share your sound selection!

-

+

+ Share your sound selection! +

+

Copy and send the following link to the person you want to share your selection with.

-
diff --git a/src/components/sounds/sound/range/range.tsx b/src/components/sounds/sound/range/range.tsx index d4022fb..0c08943 100644 --- a/src/components/sounds/sound/range/range.tsx +++ b/src/components/sounds/sound/range/range.tsx @@ -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 ( setIsAnimating(false)} onAnimationStart={() => setIsAnimating(true)} > - {showAll ? 'Show Less' : 'Show More'} + {showAll ? t('showLess') : t('showMore')} diff --git a/src/components/toolbar/menu/item/item.tsx b/src/components/toolbar/menu/item/item.tsx index 0b3b1f0..bce40d7 100644 --- a/src/components/toolbar/menu/item/item.tsx +++ b/src/components/toolbar/menu/item/item.tsx @@ -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} > - {icon} {label} + {icon} {label} {active &&
} diff --git a/src/components/toolbar/menu/items/binaural.tsx b/src/components/toolbar/menu/items/binaural.tsx index cd75f3c..7f451a1 100644 --- a/src/components/toolbar/menu/items/binaural.tsx +++ b/src/components/toolbar/menu/items/binaural.tsx @@ -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 ( - } label="Binaural Beats" onClick={open} /> + } label={t('binauralBeats')} onClick={open} /> ); } diff --git a/src/components/toolbar/menu/items/breathing-exercise.tsx b/src/components/toolbar/menu/items/breathing-exercise.tsx index 5695afb..53d2eb3 100644 --- a/src/components/toolbar/menu/items/breathing-exercise.tsx +++ b/src/components/toolbar/menu/items/breathing-exercise.tsx @@ -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 ( } - label="Breathing Exercise" + label={t('breathingExercise')} shortcut="Shift + B" onClick={open} /> diff --git a/src/components/toolbar/menu/items/countdown.tsx b/src/components/toolbar/menu/items/countdown.tsx index cd3b1a6..f6b10c6 100644 --- a/src/components/toolbar/menu/items/countdown.tsx +++ b/src/components/toolbar/menu/items/countdown.tsx @@ -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 ( } - label="Countdown Timer" + label={t('countdownTimer')} shortcut="Shift + C" onClick={open} /> diff --git a/src/components/toolbar/menu/items/donate.tsx b/src/components/toolbar/menu/items/donate.tsx index ee21235..1cfb309 100644 --- a/src/components/toolbar/menu/items/donate.tsx +++ b/src/components/toolbar/menu/items/donate.tsx @@ -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 ( } - label="Buy Me a Coffee" + label={t('buyMeACoffee')} /> ); } diff --git a/src/components/toolbar/menu/items/isochronic.tsx b/src/components/toolbar/menu/items/isochronic.tsx index 71427ac..aed0544 100644 --- a/src/components/toolbar/menu/items/isochronic.tsx +++ b/src/components/toolbar/menu/items/isochronic.tsx @@ -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 } label="Isochronic Tones" onClick={open} />; + const { t } = useTranslation(); + + return } label={t('isochronicTones')} onClick={open} />; } diff --git a/src/components/toolbar/menu/items/lofi.tsx b/src/components/toolbar/menu/items/lofi.tsx index ea9d9ab..a412ecd 100644 --- a/src/components/toolbar/menu/items/lofi.tsx +++ b/src/components/toolbar/menu/items/lofi.tsx @@ -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 ( } - label="Lofi Music Player" + label={t('lofiMusicPlayer')} onClick={open} /> ); diff --git a/src/components/toolbar/menu/items/notepad.tsx b/src/components/toolbar/menu/items/notepad.tsx index ef0ff39..d626c6a 100644 --- a/src/components/toolbar/menu/items/notepad.tsx +++ b/src/components/toolbar/menu/items/notepad.tsx @@ -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 ( } - label="Notepad" + label={t('notepad')} shortcut="Shift + N" onClick={open} /> diff --git a/src/components/toolbar/menu/items/pomodoro.tsx b/src/components/toolbar/menu/items/pomodoro.tsx index 43473b5..ea2754c 100644 --- a/src/components/toolbar/menu/items/pomodoro.tsx +++ b/src/components/toolbar/menu/items/pomodoro.tsx @@ -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 ( } - label="Pomodoro" + label={t('pomodoro')} shortcut="Shift + P" onClick={open} /> diff --git a/src/components/toolbar/menu/items/presets.tsx b/src/components/toolbar/menu/items/presets.tsx index a78b17d..33c5283 100644 --- a/src/components/toolbar/menu/items/presets.tsx +++ b/src/components/toolbar/menu/items/presets.tsx @@ -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 ( } - label="Your Presets" + label={t('presets')} + data-i18n="navigation.presets" shortcut="Shift + Alt + P" onClick={open} /> diff --git a/src/components/toolbar/menu/items/share.tsx b/src/components/toolbar/menu/items/share.tsx index 7bda116..43c1fe4 100644 --- a/src/components/toolbar/menu/items/share.tsx +++ b/src/components/toolbar/menu/items/share.tsx @@ -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 ( } - label="Share Sounds" + label={t('share')} shortcut="Shift + S" onClick={open} /> diff --git a/src/components/toolbar/menu/items/shortcuts.tsx b/src/components/toolbar/menu/items/shortcuts.tsx index fc9f4e1..5b3fb77 100644 --- a/src/components/toolbar/menu/items/shortcuts.tsx +++ b/src/components/toolbar/menu/items/shortcuts.tsx @@ -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 ( } - label="Shortcuts" + label={t('shortcuts')} shortcut="Shift + H" onClick={open} /> diff --git a/src/components/toolbar/menu/items/shuffle.tsx b/src/components/toolbar/menu/items/shuffle.tsx index 722853d..7c7f497 100644 --- a/src/components/toolbar/menu/items/shuffle.tsx +++ b/src/components/toolbar/menu/items/shuffle.tsx @@ -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() { } - label="Shuffle Sounds" + label={t('shuffleSounds')} onClick={shuffle} /> ); diff --git a/src/components/toolbar/menu/items/sleep-timer.tsx b/src/components/toolbar/menu/items/sleep-timer.tsx index 0ecfa9b..a66da25 100644 --- a/src/components/toolbar/menu/items/sleep-timer.tsx +++ b/src/components/toolbar/menu/items/sleep-timer.tsx @@ -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 ( } - label="Sleep Timer" + label={t('sleepTimer')} shortcut="Shift + Alt + T" onClick={open} /> diff --git a/src/components/toolbar/menu/items/source.tsx b/src/components/toolbar/menu/items/source.tsx index 668fcde..cab5e6a 100644 --- a/src/components/toolbar/menu/items/source.tsx +++ b/src/components/toolbar/menu/items/source.tsx @@ -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 ( } - label="Source Code" + label={t('sourceCode')} /> ); } diff --git a/src/components/toolbar/menu/items/todo.tsx b/src/components/toolbar/menu/items/todo.tsx index f2dbc1e..d1f62bc 100644 --- a/src/components/toolbar/menu/items/todo.tsx +++ b/src/components/toolbar/menu/items/todo.tsx @@ -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 ( } - label="Todo Checklist" + label={t('todoChecklist')} shortcut="Shift + T" onClick={open} /> diff --git a/src/components/toolbar/menu/menu.tsx b/src/components/toolbar/menu/menu.tsx index 33d8f51..d7f39b5 100644 --- a/src/components/toolbar/menu/menu.tsx +++ b/src/components/toolbar/menu/menu.tsx @@ -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() {
setIsOpen(o)}> - @@ -147,7 +149,7 @@ export function Menu() {
- + ; + + // Sounds + sounds: Record>; + + // 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 = { + 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; +} \ No newline at end of file diff --git a/src/data/sounds/animals.tsx b/src/data/sounds/animals.tsx index c248930..c587c95 100644 --- a/src/data/sounds/animals.tsx +++ b/src/data/sounds/animals.tsx @@ -31,98 +31,115 @@ export const animals: Category = { icon: , id: 'birds', label: 'Birds', + dataI18n: 'sounds.animals.birds', src: getAssetPath('/sounds/animals/birds.mp3'), }, { icon: , id: 'seagulls', label: 'Seagulls', + dataI18n: 'sounds.animals.seagulls', src: getAssetPath('/sounds/animals/seagulls.mp3'), }, { icon: , id: 'crickets', label: 'Crickets', + dataI18n: 'sounds.animals.crickets', src: getAssetPath('/sounds/animals/crickets.mp3'), }, { icon: , id: 'wolf', label: 'Wolf', + dataI18n: 'sounds.animals.wolves', src: getAssetPath('/sounds/animals/wolf.mp3'), }, { icon: , id: 'owl', label: 'Owl', + dataI18n: 'sounds.animals.owl', src: getAssetPath('/sounds/animals/owl.mp3'), }, { icon: , id: 'frog', label: 'Frog', + dataI18n: 'sounds.animals.frogs', src: getAssetPath('/sounds/animals/frog.mp3'), }, { icon: , id: 'dog-barking', label: 'Dog Barking', + dataI18n: 'sounds.animals.dogs', src: getAssetPath('/sounds/animals/dog-barking.mp3'), }, { icon: , id: 'horse-gallop', label: 'Horse Gallop', + dataI18n: 'sounds.animals.horses', src: getAssetPath('/sounds/animals/horse-gallop.mp3'), }, { icon: , id: 'cat-purring', label: 'Cat Purring', + dataI18n: 'sounds.animals.cats', src: getAssetPath('/sounds/animals/cat-purring.mp3'), }, { icon: , id: 'crows', label: 'Crows', + dataI18n: 'sounds.animals.crows', src: getAssetPath('/sounds/animals/crows.mp3'), }, { icon: , id: 'whale', label: 'Whale', + dataI18n: 'sounds.animals.whale', src: getAssetPath('/sounds/animals/whale.mp3'), }, { icon: , id: 'beehive', label: 'Beehive', + dataI18n: 'sounds.animals.beehive', src: getAssetPath('/sounds/animals/beehive.mp3'), }, { icon: , id: 'woodpecker', label: 'Woodpecker', + dataI18n: 'sounds.animals.woodpecker', src: getAssetPath('/sounds/animals/woodpecker.mp3'), }, { icon: , id: 'chickens', label: 'Chickens', + dataI18n: 'sounds.animals.chickens', src: getAssetPath('/sounds/animals/chickens.mp3'), }, { icon: , id: 'cows', label: 'Cows', + dataI18n: 'sounds.animals.cows', src: getAssetPath('/sounds/animals/cows.mp3'), }, { icon: , id: 'sheep', label: 'Sheep', + dataI18n: 'sounds.animals.sheep', src: getAssetPath('/sounds/animals/sheep.mp3'), }, ], title: 'Animals', + dataI18n: 'categories.animals', }; diff --git a/src/data/sounds/nature.tsx b/src/data/sounds/nature.tsx index e3d5b8d..20eebf6 100644 --- a/src/data/sounds/nature.tsx +++ b/src/data/sounds/nature.tsx @@ -21,74 +21,87 @@ export const nature: Category = { icon: , id: 'river', label: 'River', + dataI18n: 'sounds.nature.river', src: getAssetPath('/sounds/nature/river.mp3'), }, { icon: , id: 'waves', label: 'Waves', + dataI18n: 'sounds.nature.waves', src: getAssetPath('/sounds/nature/waves.mp3'), }, { icon: , id: 'campfire', label: 'Campfire', + dataI18n: 'sounds.nature.campfire', src: getAssetPath('/sounds/nature/campfire.mp3'), }, { icon: , id: 'wind', label: 'Wind', + dataI18n: 'sounds.nature.wind', src: getAssetPath('/sounds/nature/wind.mp3'), }, { icon: , id: 'howling-wind', label: 'Howling Wind', + dataI18n: 'sounds.nature.howlingWind', src: getAssetPath('/sounds/nature/howling-wind.mp3'), }, { icon: , id: 'wind-in-trees', label: 'Wind in Trees', + dataI18n: 'sounds.nature.windInTrees', src: getAssetPath('/sounds/nature/wind-in-trees.mp3'), }, { icon: , id: 'waterfall', label: 'Waterfall', + dataI18n: 'sounds.nature.waterfall', src: getAssetPath('/sounds/nature/waterfall.mp3'), }, { icon: , id: 'walk-in-snow', label: 'Walk in Snow', + dataI18n: 'sounds.nature.walkInSnow', src: getAssetPath('/sounds/nature/walk-in-snow.mp3'), }, { icon: , id: 'walk-on-leaves', label: 'Walk on Leaves', + dataI18n: 'sounds.nature.walkOnLeaves', src: getAssetPath('/sounds/nature/walk-on-leaves.mp3'), }, { icon: , id: 'walk-on-gravel', label: 'Walk on Gravel', + dataI18n: 'sounds.nature.walkOnGravel', src: getAssetPath('/sounds/nature/walk-on-gravel.mp3'), }, { icon: , id: 'droplets', label: 'Droplets', + dataI18n: 'sounds.nature.droplets', src: getAssetPath('/sounds/nature/droplets.mp3'), }, { icon: , id: 'jungle', label: 'Jungle', + dataI18n: 'sounds.nature.jungle', src: getAssetPath('/sounds/nature/jungle.mp3'), }, ], title: 'Nature', + dataI18n: 'categories.nature', }; diff --git a/src/data/sounds/noise.tsx b/src/data/sounds/noise.tsx index e0e7f39..d91f9ab 100644 --- a/src/data/sounds/noise.tsx +++ b/src/data/sounds/noise.tsx @@ -13,20 +13,24 @@ export const noise: Category = { icon: , id: 'white-noise', label: 'White Noise', + dataI18n: 'sounds.noise.whiteNoise', src: getAssetPath('/sounds/noise/white-noise.wav'), }, { icon: , id: 'pink-noise', label: 'Pink Noise', + dataI18n: 'sounds.noise.pinkNoise', src: getAssetPath('/sounds/noise/pink-noise.wav'), }, { icon: , id: 'brown-noise', label: 'Brown Noise', + dataI18n: 'sounds.noise.brownNoise', src: getAssetPath('/sounds/noise/brown-noise.wav'), }, ], title: 'Noise', + dataI18n: 'categories.noise', }; diff --git a/src/data/sounds/places.tsx b/src/data/sounds/places.tsx index c63ed2b..ff462d3 100644 --- a/src/data/sounds/places.tsx +++ b/src/data/sounds/places.tsx @@ -28,98 +28,115 @@ export const places: Category = { icon: , id: 'cafe', label: 'Cafe', + dataI18n: 'sounds.places.cafe', src: getAssetPath('/sounds/places/cafe.mp3'), }, { icon: , id: 'airport', label: 'Airport', + dataI18n: 'sounds.places.airport', src: getAssetPath('/sounds/places/airport.mp3'), }, { icon: , id: 'church', label: 'Church', + dataI18n: 'sounds.places.church', src: getAssetPath('/sounds/places/church.mp3'), }, { icon: , id: 'temple', label: 'Temple', + dataI18n: 'sounds.places.temple', src: getAssetPath('/sounds/places/temple.mp3'), }, { icon: , id: 'construction-site', label: 'Construction Site', + dataI18n: 'sounds.places.constructionSite', src: getAssetPath('/sounds/places/construction-site.mp3'), }, { icon: , id: 'underwater', label: 'Underwater', + dataI18n: 'sounds.places.underwater', src: getAssetPath('/sounds/places/underwater.mp3'), }, { icon: , id: 'crowded-bar', label: 'Crowded Bar', + dataI18n: 'sounds.places.crowdedBar', src: getAssetPath('/sounds/places/crowded-bar.mp3'), }, { icon: , id: 'night-village', label: 'Night Village', + dataI18n: 'sounds.places.nightVillage', src: getAssetPath('/sounds/places/night-village.mp3'), }, { icon: , id: 'subway-station', label: 'Subway Station', + dataI18n: 'sounds.places.subwayStation', src: getAssetPath('/sounds/places/subway-station.mp3'), }, { icon: , id: 'office', label: 'Office', + dataI18n: 'sounds.places.office', src: getAssetPath('/sounds/places/office.mp3'), }, { icon: , id: 'supermarket', label: 'Supermarket', + dataI18n: 'sounds.places.supermarket', src: getAssetPath('/sounds/places/supermarket.mp3'), }, { icon: , id: 'carousel', label: 'Carousel', + dataI18n: 'sounds.places.carousel', src: getAssetPath('/sounds/places/carousel.mp3'), }, { icon: , id: 'laboratory', label: 'Laboratory', + dataI18n: 'sounds.places.laboratory', src: getAssetPath('/sounds/places/laboratory.mp3'), }, { icon: , id: 'laundry-room', label: 'Laundry Room', + dataI18n: 'sounds.places.laundryRoom', src: getAssetPath('/sounds/places/laundry-room.mp3'), }, { icon: , id: 'restaurant', label: 'Restaurant', + dataI18n: 'sounds.places.restaurant', src: getAssetPath('/sounds/places/restaurant.mp3'), }, { icon: , id: 'library', label: 'Library', + dataI18n: 'sounds.places.library', src: getAssetPath('/sounds/places/library.mp3'), }, ], title: 'Places', + dataI18n: 'categories.places', }; diff --git a/src/data/sounds/rain.tsx b/src/data/sounds/rain.tsx index 1276ef6..6704a5a 100644 --- a/src/data/sounds/rain.tsx +++ b/src/data/sounds/rain.tsx @@ -20,50 +20,73 @@ export const rain: Category = { icon: , id: 'light-rain', label: 'Light Rain', + dataI18n: 'sounds.rain.lightRain', src: getAssetPath('/sounds/rain/light-rain.mp3'), }, + { + icon: , + id: 'moderate-rain', + label: 'Moderate Rain', + dataI18n: 'sounds.rain.moderateRain', + src: getAssetPath('/sounds/rain/moderate-rain.mp3'), + }, { icon: , id: 'heavy-rain', label: 'Heavy Rain', + dataI18n: 'sounds.rain.heavyRain', src: getAssetPath('/sounds/rain/heavy-rain.mp3'), }, + { + icon: , + id: 'storm', + label: 'Storm', + dataI18n: 'sounds.rain.storm', + src: getAssetPath('/sounds/rain/storm.mp3'), + }, { icon: , id: 'thunder', label: 'Thunder', + dataI18n: 'sounds.rain.thunder', src: getAssetPath('/sounds/rain/thunder.mp3'), }, { icon: , id: 'rain-on-window', label: 'Rain on Window', + dataI18n: 'sounds.rain.rainOnWindow', src: getAssetPath('/sounds/rain/rain-on-window.mp3'), }, { icon: , id: 'rain-on-car-roof', label: 'Rain on Car Roof', + dataI18n: 'sounds.rain.carRain', src: getAssetPath('/sounds/rain/rain-on-car-roof.mp3'), }, { icon: , id: 'rain-on-umbrella', label: 'Rain on Umbrella', + dataI18n: 'sounds.rain.rainOnUmbrella', src: getAssetPath('/sounds/rain/rain-on-umbrella.mp3'), }, { icon: , id: 'rain-on-tent', label: 'Rain on Tent', + dataI18n: 'sounds.rain.rainOnTent', src: getAssetPath('/sounds/rain/rain-on-tent.mp3'), }, { icon: , 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', }; diff --git a/src/data/sounds/things.tsx b/src/data/sounds/things.tsx index 3b58b2f..3f9ba4c 100644 --- a/src/data/sounds/things.tsx +++ b/src/data/sounds/things.tsx @@ -24,98 +24,115 @@ export const things: Category = { icon: , id: 'keyboard', label: 'Keyboard', + dataI18n: 'sounds.things.keyboard', src: getAssetPath('/sounds/things/keyboard.mp3'), }, { icon: , id: 'typewriter', label: 'Typewriter', + dataI18n: 'sounds.things.typewriter', src: getAssetPath('/sounds/things/typewriter.mp3'), }, { icon: , id: 'paper', label: 'Paper', + dataI18n: 'sounds.things.paper', src: getAssetPath('/sounds/things/paper.mp3'), }, { icon: , id: 'clock', label: 'Clock', + dataI18n: 'sounds.things.clock', src: getAssetPath('/sounds/things/clock.mp3'), }, { icon: , id: 'wind-chimes', label: 'Wind Chimes', + dataI18n: 'sounds.things.windChimes', src: getAssetPath('/sounds/things/wind-chimes.mp3'), }, { icon: , id: 'singing-bowl', label: 'Singing Bowl', + dataI18n: 'sounds.things.singingBowl', src: getAssetPath('/sounds/things/singing-bowl.mp3'), }, { icon: , id: 'ceiling-fan', label: 'Ceiling Fan', + dataI18n: 'sounds.things.ceilingFan', src: getAssetPath('/sounds/things/ceiling-fan.mp3'), }, { icon: , id: 'dryer', label: 'Dryer', + dataI18n: 'sounds.things.dryer', src: getAssetPath('/sounds/things/dryer.mp3'), }, { icon: , id: 'slide-projector', label: 'Slide Projector', + dataI18n: 'sounds.things.slideProjector', src: getAssetPath('/sounds/things/slide-projector.mp3'), }, { icon: , id: 'boiling-water', label: 'Boiling Water', + dataI18n: 'sounds.things.boilingWater', src: getAssetPath('/sounds/things/boiling-water.mp3'), }, { icon: , id: 'bubbles', label: 'Bubbles', + dataI18n: 'sounds.things.bubbles', src: getAssetPath('/sounds/things/bubbles.mp3'), }, { icon: , id: 'tuning-radio', label: 'Tuning Radio', + dataI18n: 'sounds.things.tuningRadio', src: getAssetPath('/sounds/things/tuning-radio.mp3'), }, { icon: , id: 'morse-code', label: 'Morse Code', + dataI18n: 'sounds.things.morseCode', src: getAssetPath('/sounds/things/morse-code.mp3'), }, { icon: , id: 'washing-machine', label: 'Washing Machine', + dataI18n: 'sounds.things.washingMachine', src: getAssetPath('/sounds/things/washing-machine.mp3'), }, { icon: , id: 'vinyl-effect', label: 'Vinyl Effect', + dataI18n: 'sounds.things.vinylEffect', src: getAssetPath('/sounds/things/vinyl-effect.mp3'), }, { icon: , id: 'windshield-wipers', label: 'Windshield Wipers', + dataI18n: 'sounds.things.windshieldWipers', src: getAssetPath('/sounds/things/windshield-wipers.mp3'), }, ], title: 'Things', + dataI18n: 'categories.things', }; diff --git a/src/data/sounds/transport.tsx b/src/data/sounds/transport.tsx index 62d2579..d74b302 100644 --- a/src/data/sounds/transport.tsx +++ b/src/data/sounds/transport.tsx @@ -15,38 +15,45 @@ export const transport: Category = { icon: , id: 'train', label: 'Train', + dataI18n: 'sounds.transport.train', src: getAssetPath('/sounds/transport/train.mp3'), }, { icon: , id: 'inside-a-train', label: 'Inside a Train', + dataI18n: 'sounds.transport.insideTrain', src: getAssetPath('/sounds/transport/inside-a-train.mp3'), }, { icon: , id: 'airplane', label: 'Airplane', + dataI18n: 'sounds.transport.airplane', src: getAssetPath('/sounds/transport/airplane.mp3'), }, { icon: , id: 'submarine', label: 'Submarine', + dataI18n: 'sounds.transport.submarine', src: getAssetPath('/sounds/transport/submarine.mp3'), }, { icon: , id: 'sailboat', label: 'Sailboat', + dataI18n: 'sounds.transport.sailboat', src: getAssetPath('/sounds/transport/sailboat.mp3'), }, { icon: , id: 'rowing-boat', label: 'Rowing Boat', + dataI18n: 'sounds.transport.rowingBoat', src: getAssetPath('/sounds/transport/rowing-boat.mp3'), }, ], title: 'Transport', + dataI18n: 'categories.transport', }; diff --git a/src/data/sounds/urban.tsx b/src/data/sounds/urban.tsx index 142745e..edfb861 100644 --- a/src/data/sounds/urban.tsx +++ b/src/data/sounds/urban.tsx @@ -16,44 +16,52 @@ export const urban: Category = { icon: , id: 'highway', label: 'Highway', + dataI18n: 'sounds.urban.highway', src: getAssetPath('/sounds/urban/highway.mp3'), }, { icon: , id: 'road', label: 'Road', + dataI18n: 'sounds.urban.road', src: getAssetPath('/sounds/urban/road.mp3'), }, { icon: , id: 'ambulance-siren', label: 'Ambulance Siren', + dataI18n: 'sounds.urban.ambulanceSiren', src: getAssetPath('/sounds/urban/ambulance-siren.mp3'), }, { icon: , id: 'busy-street', label: 'Busy Street', + dataI18n: 'sounds.urban.busyStreet', src: getAssetPath('/sounds/urban/busy-street.mp3'), }, { icon: , id: 'crowd', label: 'Crowd', + dataI18n: 'sounds.urban.crowd', src: getAssetPath('/sounds/urban/crowd.mp3'), }, { icon: , id: 'traffic', label: 'Traffic', + dataI18n: 'sounds.urban.traffic', src: getAssetPath('/sounds/urban/traffic.mp3'), }, { icon: , id: 'fireworks', label: 'Fireworks', + dataI18n: 'sounds.urban.fireworks', src: getAssetPath('/sounds/urban/fireworks.mp3'), }, ], title: 'Urban', + dataI18n: 'categories.urban', }; diff --git a/src/data/types.d.ts b/src/data/types.d.ts index 515d516..47091da 100644 --- a/src/data/types.d.ts +++ b/src/data/types.d.ts @@ -3,6 +3,7 @@ export interface Sound { id: string; label: string; src: string; + dataI18n?: string; } export type Sounds = Array; @@ -12,6 +13,7 @@ export interface Category { id: string; sounds: Sounds; title: string; + dataI18n?: string; } export type Categories = Array; diff --git a/src/helpers/translation.ts b/src/helpers/translation.ts new file mode 100644 index 0000000..9a69d78 --- /dev/null +++ b/src/helpers/translation.ts @@ -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, + }; +} \ No newline at end of file diff --git a/src/hooks/useLanguage.ts b/src/hooks/useLanguage.ts new file mode 100644 index 0000000..1d047ee --- /dev/null +++ b/src/hooks/useLanguage.ts @@ -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' + }; +} \ No newline at end of file diff --git a/src/hooks/useLocalizedSounds.ts b/src/hooks/useLocalizedSounds.ts new file mode 100644 index 0000000..bcc2b5b --- /dev/null +++ b/src/hooks/useLocalizedSounds.ts @@ -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; +} \ No newline at end of file diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts new file mode 100644 index 0000000..901008c --- /dev/null +++ b/src/hooks/useTranslation.ts @@ -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) => { + 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); +} \ No newline at end of file diff --git a/src/layouts/layout.astro b/src/layouts/layout.astro index bbc64e6..c2b8673 100644 --- a/src/layouts/layout.astro +++ b/src/layouts/layout.astro @@ -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'; --- - + @@ -40,11 +46,36 @@ const description = + + + + + {pwaInfo && } + + + diff --git a/src/pages/index.astro b/src/pages/index.astro index 94e2adc..4ab7c08 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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.'; --- - + - -