├── .babelrc ├── .devcontainer └── devcontainer.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature_request.md │ └── new_language_request.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── semantic.yml └── workflows │ ├── ci.yml │ ├── code-ql.yml │ ├── dependency-review.yml │ ├── deploy.yml │ ├── pr-ci.yml │ └── scorecards.yml ├── .gitignore ├── .mergify.yml ├── .mocharc.json ├── .npmignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── TRANSLATION.md ├── client ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── crowdin.yml ├── cypress.config.js ├── cypress ├── e2e │ └── parser.cy.js ├── fixtures │ ├── mwb_E_202309.epub │ └── w_E_202309.epub └── support │ ├── commands.js │ └── e2e.js ├── example ├── index.ts └── sample.ts ├── package-lock.json ├── package.json ├── release.config.cjs ├── rollup.config.js ├── src ├── browser │ ├── index.ts │ └── utils.browser.ts ├── classes │ └── error.ts ├── common │ ├── date_parser.ts │ ├── epub_jszip.ts │ ├── epub_validation.ts │ ├── html_utils.ts │ ├── html_validation.ts │ ├── language_rules.ts │ ├── override.ts │ ├── parser.ts │ └── parsing_rules.ts ├── locales │ ├── ch-CHS │ │ └── text.json │ ├── cmn-Hant │ │ └── text.json │ ├── de-DE │ │ └── text.json │ ├── en-LR │ │ └── text.json │ ├── en │ │ └── text.json │ ├── es-ES │ │ └── text.json │ ├── es-LSE │ │ └── text.json │ ├── et-EE │ │ └── text.json │ ├── fi-FI │ │ └── text.json │ ├── fr-FR │ │ └── text.json │ ├── ht-HT │ │ └── text.json │ ├── hu-HU │ │ └── text.json │ ├── ilo-PH │ │ └── text.json │ ├── it-IT │ │ └── text.json │ ├── ja-JP │ │ └── text.json │ ├── ko-KR │ │ └── text.json │ ├── languages.ts │ ├── mg-MG │ │ └── text.json │ ├── mg-TND │ │ └── text.json │ ├── mg-TNK │ │ └── text.json │ ├── mg-TTM │ │ └── text.json │ ├── mg-VZ │ │ └── text.json │ ├── ms-MY │ │ └── text.json │ ├── ne-NP │ │ └── text.json │ ├── nl-NL │ │ └── text.json │ ├── pl-PL │ │ └── text.json │ ├── pt-POR │ │ └── text.json │ ├── pt-TPO │ │ └── text.json │ ├── ro-RO │ │ └── text.json │ ├── ru-RU │ │ └── text.json │ ├── rw-RW │ │ └── text.json │ ├── sl-SI │ │ └── text.json │ ├── sv-SE │ │ └── text.json │ ├── sw-KE │ │ └── text.json │ ├── tl-PH │ │ └── text.json │ ├── tr-TR │ │ └── text.json │ ├── tw-TW │ │ └── text.json │ ├── uk-UA │ │ └── text.json │ ├── vi-VN │ │ └── text.json │ └── wes-PGW │ │ └── text.json ├── node │ ├── index.ts │ └── utils.node.ts └── types │ └── index.ts ├── test ├── 01_standardParsing.test.js ├── 02_enhancedParsing.test.js ├── enhancedParsing │ └── list.json ├── fixtures │ ├── mwb_CHS_202411.js │ ├── mwb_CH_202411.js │ ├── mwb_CR_202411.js │ ├── mwb_D_202411.js │ ├── mwb_ELI_202407.js │ ├── mwb_E_202411.js │ ├── mwb_F_202411.js │ ├── mwb_IL_202411.js │ ├── mwb_I_202411.js │ ├── mwb_J_202411.js │ ├── mwb_KO_202411.js │ ├── mwb_K_202411.js │ ├── mwb_MG_202411.js │ ├── mwb_ML_202501.js │ ├── mwb_M_202411.js │ ├── mwb_O_202411.js │ ├── mwb_PGW_202411.js │ ├── mwb_SV_202411.js │ ├── mwb_SW_202411.js │ ├── mwb_S_202411.js │ ├── mwb_TG_202411.js │ ├── mwb_TK_202411.js │ ├── mwb_TND_202411.js │ ├── mwb_TNK_202411.js │ ├── mwb_TPO_202411.js │ ├── mwb_U_202411.js │ ├── mwb_VT_202501.js │ ├── mwb_VZ_202411.js │ ├── mwb_X_202411.js │ ├── mwb_YW_202501.js │ ├── mwb_Z_202411.js │ ├── w_CHS_202411.js │ ├── w_CH_202411.js │ ├── w_CR_202411.js │ ├── w_D_202411.js │ ├── w_ELI_202407.js │ ├── w_E_202411.js │ ├── w_F_202411.js │ ├── w_IL_202411.js │ ├── w_I_202411.js │ ├── w_J_202411.js │ ├── w_KO_202411.js │ ├── w_K_202411.js │ ├── w_MG_202411.js │ ├── w_ML_202501.js │ ├── w_M_202411.js │ ├── w_O_202411.js │ ├── w_PGW_202411.js │ ├── w_SV_202411.js │ ├── w_SW_202411.js │ ├── w_S_202411.js │ ├── w_TG_202411.js │ ├── w_TK_202411.js │ ├── w_TND_202411.js │ ├── w_TNK_202411.js │ ├── w_TPO_202411.js │ ├── w_U_202411.js │ ├── w_VT_202501.js │ ├── w_VZ_202411.js │ ├── w_X_202411.js │ ├── w_YW_202501.js │ └── w_Z_202411.js └── standardParsing │ └── list.json ├── tsconfig.json └── tsup.config.ts /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Node.js", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye", 7 | "features": { 8 | "ghcr.io/devcontainers/features/node:1": {} 9 | }, 10 | "customizations": { 11 | "vscode": { 12 | "extensions": [ 13 | "esbenp.prettier-vscode" 14 | ] 15 | } 16 | } 17 | 18 | // Features to add to the dev container. More info: https://containers.dev/features. 19 | // "features": {}, 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "yarn install", 26 | 27 | // Configure tool-specific properties. 28 | // "customizations": {}, 29 | 30 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 31 | // "remoteUser": "root" 32 | } 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: sws2apps 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT] " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_language_request.yml: -------------------------------------------------------------------------------- 1 | name: Translation for JW EPUB Parser 2 | description: Request to enable enhanced parsing for a language 3 | labels: [i18n] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Add an issue title with this format: Enable Enhanced Parsing for [Language] 9 | - type: textarea 10 | attributes: 11 | label: 'Add a brief description for this request' 12 | placeholder: | 13 | Message body 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # Checklist: 17 | 18 | - [ ] My code follows the style guidelines of this project 19 | - [ ] I have performed a self-review of my own code 20 | - [ ] I have commented my code, particularly in hard-to-understand areas 21 | - [ ] I have made corresponding changes to the documentation 22 | - [ ] My changes generate no new warnings 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'daily' 8 | 9 | # Maintain dependencies for npm 10 | - package-ecosystem: 'npm' 11 | directory: '/' 12 | schedule: 13 | interval: 'daily' 14 | allow: 15 | - dependency-type: 'production' 16 | - dependency-type: 'development' 17 | commit-message: 18 | prefix: 'feat(deps)' 19 | prefix-development: 'build(deps)' 20 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleOnly: true 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main, beta, alpha] 7 | schedule: 8 | - cron: '0 3 * * *' 9 | 10 | permissions: read-all 11 | 12 | jobs: 13 | test_unit: 14 | name: Run Test Units 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: 'Checkout code' 19 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 20 | 21 | - name: Use Node.js LTS version 22 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af 23 | with: 24 | node-version: lts/Iron 25 | 26 | - name: Install dependency 27 | run: npm ci 28 | 29 | - name: Build output 30 | run: npm run build 31 | 32 | - name: Run Test Units 33 | run: npm run test 34 | 35 | - name: Browser version testing 36 | uses: cypress-io/github-action@57b70560982e6a11d23d4b8bec7f8a487cdbb71b 37 | with: 38 | command-prefix: '--' 39 | install: false 40 | browser: chrome 41 | build: npm run setup:client 42 | start: npm run start:client 43 | record: true 44 | env: 45 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_JW_EPUB_PARSER_RECORD_KEY }} 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | CYPRESS_PROJECT_ID: dvtrvb 48 | -------------------------------------------------------------------------------- /.github/workflows/code-ql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [main, beta, alpha] 6 | pull_request: 7 | branches: [main, beta, alpha] 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | codeql: 13 | name: Code QL 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | 23 | steps: 24 | - name: Checkout repository for code analysis 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 26 | 27 | - name: Initialize CodeQL 28 | uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae 29 | with: 30 | languages: javascript 31 | queries: security-extended 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae 35 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Review 2 | 3 | on: [pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | dependency-review: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout for dependency review 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 15 | 16 | - name: Running Dependency Review 17 | uses: actions/dependency-review-action@0659a74c94536054bfa5aeb92241f70d680cc78e 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: read-all 7 | 8 | jobs: 9 | deploy_dev: 10 | name: Preparing Alpha release 11 | if: ${{ github.repository == 'sws2apps/jw-epub-parser' && github.ref == 'refs/heads/alpha' }} 12 | environment: 13 | name: Development 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | id-token: write 18 | 19 | steps: 20 | - name: Checkout for release preparation 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 22 | with: 23 | ref: alpha 24 | persist-credentials: false 25 | 26 | - name: Use Node.js LTS version 27 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af 28 | with: 29 | node-version: lts/Iron 30 | 31 | - name: Install package dependencies 32 | run: npm ci 33 | 34 | - name: Semantic Release 35 | id: semantic 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | run: npx semantic-release 40 | 41 | deploy_stg: 42 | name: Preparing Beta release 43 | if: ${{ github.repository == 'sws2apps/jw-epub-parser' && github.ref == 'refs/heads/beta' }} 44 | environment: 45 | name: Staging 46 | runs-on: ubuntu-latest 47 | permissions: 48 | contents: read 49 | id-token: write 50 | 51 | steps: 52 | - name: Checkout for release preparation 53 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 54 | with: 55 | ref: beta 56 | persist-credentials: false 57 | 58 | - name: Use Node.js LTS version 59 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af 60 | with: 61 | node-version: lts/Iron 62 | 63 | - name: Install package dependencies 64 | run: npm ci 65 | 66 | - name: Semantic Release 67 | id: semantic 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | run: npx semantic-release 72 | 73 | deploy_prod: 74 | name: Preparing Production release 75 | if: ${{ github.repository == 'sws2apps/jw-epub-parser' && github.ref == 'refs/heads/main' }} 76 | environment: 77 | name: Production 78 | runs-on: ubuntu-latest 79 | permissions: 80 | contents: read 81 | id-token: write 82 | 83 | steps: 84 | - name: Checkout for release preparation 85 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 86 | with: 87 | ref: main 88 | persist-credentials: false 89 | 90 | - name: Use Node.js LTS version 91 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af 92 | with: 93 | node-version: lts/Iron 94 | 95 | - name: Install package dependencies 96 | run: npm ci 97 | 98 | - name: Semantic Release 99 | id: semantic 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 102 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 103 | run: npx semantic-release 104 | -------------------------------------------------------------------------------- /.github/workflows/pr-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI (Pull Requests) 2 | 3 | on: 4 | pull_request: 5 | branches: [main, beta, alpha] 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | test_unit: 11 | name: Run Test Units 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: 'Checkout code' 16 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 17 | 18 | - name: Use Node.js LTS version 19 | uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af 20 | with: 21 | node-version: lts/Iron 22 | 23 | - name: Install dependency 24 | run: npm ci 25 | 26 | - name: Build output 27 | run: npm run build 28 | 29 | - name: Run Test Units 30 | run: npm run test 31 | 32 | - name: Browser version testing 33 | uses: cypress-io/github-action@57b70560982e6a11d23d4b8bec7f8a487cdbb71b 34 | with: 35 | command-prefix: '--' 36 | install: false 37 | browser: chrome 38 | build: npm run setup:client 39 | start: npm run start:client 40 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | name: Scorecards 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: read-all 8 | 9 | jobs: 10 | scorecards: 11 | name: Scorecards Analysis 12 | runs-on: ubuntu-latest 13 | permissions: 14 | # Needed to upload the results to code-scanning dashboard. 15 | security-events: write 16 | # Used to receive a badge. (Upcoming feature) 17 | id-token: write 18 | actions: read 19 | contents: read 20 | 21 | steps: 22 | - name: 'Checkout code' 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 24 | with: 25 | persist-credentials: false 26 | 27 | - name: 'Run analysis' 28 | uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 29 | with: 30 | results_file: results.sarif 31 | results_format: sarif 32 | repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} 33 | publish_results: true 34 | 35 | - name: 'Upload to code-scanning' 36 | uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae 37 | with: 38 | sarif_file: results.sarif 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | sample 4 | 5 | .env 6 | 7 | *.epub -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - and: 5 | - author=dependabot[bot] 6 | - check-success=Run Test Units 7 | - check-success~=^security/snyk 8 | - or: 9 | - title~=^chore\(deps\). [w+]|[^\s]+ from ([\d]+)\..+ to \1\. 10 | - title~=^feat\(deps\). [w+]|[^\s]+ from ([\d]+)\..+ to \1\. 11 | actions: 12 | review: 13 | type: APPROVE 14 | message: Automatically approving dependabot 15 | merge: 16 | method: rebase -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "diff": true, 3 | "extension": ["js", "cjs", "mjs"], 4 | "package": "./package.json", 5 | "reporter": "spec", 6 | "slow": "75", 7 | "timeout": "160000", 8 | "ui": "bdd", 9 | "watch-files": ["lib/**/*.js", "test/**/*.js"], 10 | "watch-ignore": ["lib/vendor"] 11 | } 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | *.test.js 3 | 4 | .github 5 | 6 | release.config.cjs 7 | rollup.config.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | provenance=true -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.jsxSingleQuote": true, 3 | "prettier.printWidth": 120, 4 | "prettier.singleQuote": true 5 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | sws2apps.notification@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 2.0, available at 73 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 74 | 75 | For answers to common questions about this code of conduct, see the FAQ at 76 | https://www.contributor-covenant.org/faq. Translations are available at 77 | https://www.contributor-covenant.org/translations. 78 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | JW EPUB Parser is one of the utilities that is developped by the [Scheduling Workbox System (SWS)](https://github.com/sws2apps) team. But we are also more than happy to receive support from those who are very intersted to assist us. Hopefully this document makes the process for contributing clear and answers some questions that you may have. 4 | 5 | Please make sure that you have read the [code of conduct](https://github.com/sws2apps/jw-epub-parser/blob/main/CODE_OF_CONDUCT.md) before continuing. 6 | 7 | ## Semantic Versioning 8 | 9 | JW EPUB Parser follows semantic versioning. We release patch versions for bugfixes, minor versions for new features or non-essential changes, and major versions for any breaking changes. Every significant change is documented in the [changelog](https://github.com/sws2apps/jw-epub-parser/blob/main/CHANGELOG.md) file. 10 | 11 | ## Branch Organization 12 | 13 | We used three different branches to make production, beta and alpha releases of SWS Pocket: 14 | 15 | | branch | whats for | 16 | | :----- | :--------------------------------------------------------------------------------------------------------- | 17 | | main | making production release of JW EPUB Parser: bug fix for the current version will be queued in this branch | 18 | | beta | making beta release of JW EPUB Parser: new feature will be queued in this branch | 19 | | alpha | making alpha release of JW EPUB Parser: major update to the application will be queued in this branch | 20 | 21 | ## Bugs 22 | 23 | ### Known Issues and Report 24 | 25 | We are using [GitHub Issues](https://github.com/sws2apps/jw-epub-parser/issues) to keep track of bugs fix. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist. 26 | 27 | ### Security Bugs 28 | 29 | Please do not report security bugs in the public issues; go through the process outlined on the [Security Policy](https://github.com/sws2apps/jw-epub-parser/blob/main/SECURITY.md). 30 | 31 | ## Proposing a Change 32 | 33 | If you intend to add new features or suggest major changes to JW EPUB Parser, check first that your idea is not yet in our tracking issues list. If not, we recommend creating a new [discussion first](https://github.com/sws2apps/jw-epub-parser/discussions/categories/ideas). This lets us reach an agreement on your proposal before you put significant effort into it. After it has been approved, please create [new issue](https://github.com/sws2apps/jw-epub-parser/issues), and choose the correct template. 34 | 35 | If you’re only fixing a bug, it’s fine to submit a pull request right away but we still recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue. 36 | 37 | ## Contribution Prerequisites 38 | 39 | - You have the latest version of [Node](https://nodejs.org) and [Git](https://git-scm.com) installed 40 | - You will be working on one item at a time. 41 | - If you do not have it yet, fork the repository. Clone it if you will work locally. 42 | - If you have already forked and clone the repository, make sure that it is in sync with the upstream repository ([Syncing a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)). 43 | - Run `npm i` to install the needed dependencies 44 | 45 | ## Sending a Pull Request (PR) 46 | 47 | We are monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation. We’ll do our best to provide updates and feedback throughout the process. 48 | 49 | **Before submitting a PR**, please make sure the following is done: 50 | 51 | - Run `npm build` to build the module first. 52 | - Then run `npm test` to make sure that your changes pass the test. 53 | 54 | **When commiting your changes**, we recommend the following commands to be run: 55 | 56 | - Check again if your forked repository or your local copy is up to date with upstream. ([Syncing a fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork)). 57 | - Resolve conflicts if any. 58 | - Commit and push your changes to your forked repository. 59 | 60 | **When your proposed changes are in the forked repository on GitHub**: 61 | 62 | - Create your PR. 63 | - Make sure the title follows the [conventional-changelog](https://github.com/semantic-release/semantic-release#commit-message-format) format, depending on what item or issue you have been working on. Failure to set this accordingly will cause your pull request to be discarded. 64 | 65 | You will receive a notification and be informed when your PR is published on beta, or alpha, or in production. 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Scheduling Workbox System 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | You can report a vulnerability by filling the [GitHub Security Advisories](https://github.com/sws2apps/jw-epub-parser/security/advisories) form. We will try to do our best to respond you and handle the matter as soon as possible. 6 | -------------------------------------------------------------------------------- /TRANSLATION.md: -------------------------------------------------------------------------------- 1 | # JW EPUB Parser Translation Guide 2 | 3 | ## Get started 4 | 5 | To enhance parsing for a language, make sure first that EPUB is available for that language on jw.org. 6 | 7 | 1. Open the [JW EPUB Parser](https://crowdin.com/project/cpe-jw-epub-parser) project on Crowdin 8 | 2. Find your locale and start translation. Find more details in [guide for volunteer translators](https://support.crowdin.com/for-volunteer-translators/) 9 | 10 | All translated and approved content will be pushed to this repo automatically. You don't need to create any PRs with translation. 11 | 12 | Original source can be found in [/locales/en](https://github.com/sws2apps/jw-epub-parser/tree/main/src/locales/en). If you find any problem with original source, please create a PR with changes directly to `/locales/en`. Crowdin automatically pull all updates within 1 hour. 13 | 14 | ### I can't find my language on Crowdin 15 | 16 | Please create a [new issue](https://github.com/sws2apps/jw-epub-parser/issues/new?template=new_language_request.yml) in this repo. We would be happy to add the new language so that you can start the translation on Crowdin. 17 | -------------------------------------------------------------------------------- /client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JW EPUB Parser (in the browser) 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-ts", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "file-select-dialog": "^1.5.4", 14 | "json-formatter-js": "^2.5.23", 15 | "jw-epub-parser": "file:..", 16 | "react": "^19.0.0", 17 | "react-dom": "^19.0.0" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.21.0", 21 | "@types/react": "^19.0.10", 22 | "@types/react-dom": "^19.0.4", 23 | "@vitejs/plugin-react": "^4.3.4", 24 | "eslint": "^9.21.0", 25 | "eslint-plugin-react-hooks": "^5.1.0", 26 | "eslint-plugin-react-refresh": "^0.4.19", 27 | "globals": "^15.15.0", 28 | "typescript": "~5.7.2", 29 | "typescript-eslint": "^8.24.1", 30 | "vite": "^6.3.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { loadEPUB, MWBSchedule, WSchedule } from 'jw-epub-parser'; 2 | import JSONFormatter from 'json-formatter-js'; 3 | 4 | function App() { 5 | const handleOpenMWBEPUB = async (file: File) => { 6 | try { 7 | const data: MWBSchedule = await loadEPUB(file); 8 | 9 | const formatter = new JSONFormatter(data); 10 | 11 | const result = document.querySelector('#mwb_parser_result')!; 12 | result.innerHTML = ''; 13 | result.appendChild(formatter.render()); 14 | } catch (err) { 15 | console.error(err); 16 | } 17 | }; 18 | 19 | const handleOpenWEPUB = async (file: File) => { 20 | try { 21 | const data: WSchedule = await loadEPUB(file); 22 | const formatter = new JSONFormatter(data); 23 | 24 | const result = document.querySelector('#w_parser_result')!; 25 | result.innerHTML = ''; 26 | result.appendChild(formatter.render()); 27 | } catch (err) { 28 | console.error(err); 29 | } 30 | }; 31 | 32 | return ( 33 |
34 |

JW EPUB Parser

35 |

Meeting Workbook Data

36 | handleOpenMWBEPUB(e.target.files!.item(0)!)}> 37 |
38 |

Watchtower Study Data

39 | handleOpenWEPUB(e.target.files!.item(0)!)}> 40 |
41 |
42 | ); 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | } 6 | 7 | body { 8 | padding: 0 30px 9 | } 10 | 11 | h1 { 12 | font-size: 3.2em; 13 | line-height: 1.1; 14 | } -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 5000, 9 | host: true, 10 | }, 11 | preview: { 12 | port: 5000, 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | 'pull_request_title': 'chore(locales): updated translation from Crowdin' 2 | 'pull_request_labels': ['crowdin'] 3 | 4 | files: 5 | - source: /src/locales/en/*.json 6 | translation: /src/locales/%locale%/%original_file_name% 7 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | projectId: 'dvtrvb', 5 | e2e: { 6 | setupNodeEvents(on, config) { 7 | // implement node event listeners here 8 | }, 9 | baseUrl: 'http://localhost:5000', 10 | video: false, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /cypress/e2e/parser.cy.js: -------------------------------------------------------------------------------- 1 | describe('Testing JW EPUB Parser Module in the browser', () => { 2 | beforeEach(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('correctly parses mwb epub file', () => { 7 | cy.get('[data-test=mwb_file]').selectFile('cypress/fixtures/mwb_E_202309.epub'); 8 | cy.get('.json-formatter-toggler').should('exist'); 9 | }); 10 | 11 | it('correctly parses w epub file', () => { 12 | cy.get('[data-test=w_file]').selectFile('cypress/fixtures/w_E_202309.epub'); 13 | cy.get('.json-formatter-toggler').should('exist'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /cypress/fixtures/mwb_E_202309.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sws2apps/jw-epub-parser/67ddc6ad4fd74e09b17ef270bb829c298076d53a/cypress/fixtures/mwb_E_202309.epub -------------------------------------------------------------------------------- /cypress/fixtures/w_E_202309.epub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sws2apps/jw-epub-parser/67ddc6ad4fd74e09b17ef270bb829c298076d53a/cypress/fixtures/w_E_202309.epub -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /example/index.ts: -------------------------------------------------------------------------------- 1 | import { fetchData } from './sample.js'; 2 | 3 | const runLiveCommand = async () => { 4 | const languageIndex = process.argv.indexOf('language'); 5 | if (languageIndex === -1) { 6 | console.error('language missing from arguments'); 7 | return; 8 | } 9 | 10 | const issueIndex = process.argv.indexOf('issue'); 11 | const pubIndex = process.argv.indexOf('pub'); 12 | 13 | if (issueIndex >= 0 && pubIndex === -1) { 14 | console.error('issue date was provided but pub type is missing'); 15 | return; 16 | } 17 | 18 | if (pubIndex >= 0 && issueIndex === -1) { 19 | console.error('pub type was provided but issue date is missing'); 20 | return; 21 | } 22 | 23 | const language = process.argv[languageIndex + 1].toUpperCase(); 24 | const issue = issueIndex >= 0 ? process.argv[issueIndex + 1] : undefined; 25 | const pub = pubIndex >= 0 ? process.argv[pubIndex + 1] : undefined; 26 | 27 | console.time(); 28 | const data = await fetchData(language, issue, pub); 29 | 30 | console.log(data); 31 | console.timeEnd(); 32 | }; 33 | 34 | runLiveCommand(); 35 | -------------------------------------------------------------------------------- /example/sample.ts: -------------------------------------------------------------------------------- 1 | import { loadEPUB } from '../src/node/index.js'; 2 | 3 | const JW_CDN = 'https://b.jw-cdn.org/apis/pub-media/GETPUBMEDIALINKS?'; 4 | const WOL_E = 'https://wol.jw.org/wol/dt/r1/lp-e'; 5 | 6 | const months = [ 7 | 'January', 8 | 'February', 9 | 'March', 10 | 'April', 11 | 'May', 12 | 'June', 13 | 'July', 14 | 'August', 15 | 'September', 16 | 'October', 17 | 'November', 18 | 'December', 19 | ]; 20 | 21 | type issue = { 22 | issueDate: string; 23 | currentYear: number; 24 | language: string; 25 | hasEPUB: any; 26 | }; 27 | 28 | const fetchIssueData = async (issue: issue): Promise => { 29 | try { 30 | if (issue.hasEPUB) { 31 | const epubFile = issue.hasEPUB[0].file; 32 | const epubUrl = epubFile.url; 33 | 34 | const epubData = await loadEPUB({ url: epubUrl }); 35 | return epubData; 36 | } 37 | 38 | if (!issue.hasEPUB) { 39 | return []; 40 | } 41 | } catch (err: any) { 42 | throw new Error(err); 43 | } 44 | }; 45 | 46 | export const fetchData = async (language: string, issue: string | undefined, pub: string | undefined) => { 47 | let data = []; 48 | 49 | if (!issue && !pub) { 50 | for await (const pub of ['mwb', 'w']) { 51 | const issues = []; 52 | 53 | if (pub === 'mwb') { 54 | let notFound = false; 55 | 56 | // get current issue 57 | const today = new Date(); 58 | const day = today.getDay(); 59 | const diff = today.getDate() - day + (day === 0 ? -6 : 1); 60 | const weekDate = new Date(today.setDate(diff)); 61 | const validDate = weekDate.setMonth(weekDate.getMonth()); 62 | 63 | const startDate = new Date(validDate); 64 | const currentMonth = startDate.getMonth() + 1; 65 | const monthOdd = currentMonth % 2 === 0 ? false : true; 66 | let monthMwb = monthOdd ? currentMonth : currentMonth - 1; 67 | let currentYear = startDate.getFullYear(); 68 | 69 | do { 70 | const issueDate = currentYear + String(monthMwb).padStart(2, '0'); 71 | const url = 72 | JW_CDN + 73 | new URLSearchParams({ 74 | langwritten: language, 75 | pub, 76 | output: 'json', 77 | issue: issueDate, 78 | }); 79 | 80 | const res = await fetch(url); 81 | 82 | if (res.status === 200) { 83 | const result = await res.json(); 84 | const hasEPUB = result.files[language].EPUB; 85 | 86 | issues.push({ issueDate, currentYear, language, hasEPUB: hasEPUB }); 87 | } 88 | 89 | if (res.status === 404) { 90 | notFound = true; 91 | } 92 | 93 | // assigning next issue 94 | monthMwb = monthMwb + 2; 95 | if (monthMwb === 13) { 96 | monthMwb = 1; 97 | currentYear++; 98 | } 99 | } while (notFound === false); 100 | } 101 | 102 | if (pub === 'w') { 103 | let notFound = false; 104 | 105 | // get w current issue 106 | const today = new Date(); 107 | const url = `${WOL_E}/${today.getFullYear()}/${today.getMonth() + 1}/${today.getDate()}`; 108 | 109 | const res = await fetch(url); 110 | const data = await res.json(); 111 | 112 | const wData = data.items.find((item: any) => item.classification === 68); 113 | const publicationTitle = wData.publicationTitle; 114 | 115 | const findYear = /\b\d{4}\b/; 116 | const array = findYear.exec(publicationTitle); 117 | let currentYear = +array![0]; 118 | 119 | const monthsRegex = `(${months.join('|')})`; 120 | 121 | const regex = new RegExp(monthsRegex); 122 | const array2 = regex.exec(publicationTitle); 123 | 124 | let monthW = months.findIndex((month) => month === array2![0]) + 1; 125 | 126 | do { 127 | const issueDate = currentYear + String(monthW).padStart(2, '0'); 128 | const url = 129 | JW_CDN + 130 | new URLSearchParams({ 131 | langwritten: language, 132 | pub, 133 | output: 'json', 134 | issue: issueDate, 135 | }); 136 | 137 | console.log(url); 138 | 139 | const res = await fetch(url); 140 | 141 | if (res.status === 200) { 142 | const result = await res.json(); 143 | const hasEPUB = result.files[language].EPUB; 144 | 145 | issues.push({ issueDate, currentYear, language, hasEPUB: hasEPUB }); 146 | } 147 | 148 | if (res.status === 404) { 149 | notFound = true; 150 | } 151 | 152 | // assigning next issue 153 | monthW = monthW + 1; 154 | if (monthW === 13) { 155 | monthW = 1; 156 | currentYear++; 157 | } 158 | } while (notFound === false); 159 | } 160 | 161 | if (issues.length > 0) { 162 | const fetchSource1 = fetchIssueData(issues[0]); 163 | const fetchSource2 = issues.length > 1 ? fetchIssueData(issues[1]) : Promise.resolve([]); 164 | const fetchSource3 = issues.length > 2 ? fetchIssueData(issues[2]) : Promise.resolve([]); 165 | const fetchSource4 = issues.length > 3 ? fetchIssueData(issues[3]) : Promise.resolve([]); 166 | const fetchSource5 = issues.length > 4 ? fetchIssueData(issues[4]) : Promise.resolve([]); 167 | const fetchSource6 = issues.length > 5 ? fetchIssueData(issues[5]) : Promise.resolve([]); 168 | const fetchSource7 = issues.length > 6 ? fetchIssueData(issues[6]) : Promise.resolve([]); 169 | 170 | const allData = await Promise.all([ 171 | fetchSource1, 172 | fetchSource2, 173 | fetchSource3, 174 | fetchSource4, 175 | fetchSource5, 176 | fetchSource6, 177 | fetchSource7, 178 | ]); 179 | 180 | for (let z = 0; z < allData.length; z++) { 181 | const tempObj: any = allData[z]; 182 | if (tempObj.length > 0) { 183 | for (const src of tempObj) { 184 | const date = src.mwb_week_date || src.w_study_date; 185 | 186 | const prevSrc = data.find((item) => item.mwb_week_date === date || item.w_study_date === date); 187 | 188 | if (prevSrc) { 189 | Object.assign(prevSrc, src); 190 | } 191 | 192 | if (!prevSrc) { 193 | data.push(src); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | for (const src of data) { 202 | if (src.mwb_week_date) { 203 | src.week_date = src.mwb_week_date; 204 | delete src.mwb_week_date; 205 | } 206 | 207 | if (src.w_study_date) { 208 | src.week_date = src.w_study_date; 209 | delete src.w_study_date; 210 | } 211 | } 212 | } 213 | 214 | if (issue && pub) { 215 | const url = JW_CDN + new URLSearchParams({ langwritten: language, pub, output: 'json', issue }); 216 | 217 | const res = await fetch(url); 218 | 219 | if (res.status === 200) { 220 | const result = await res.json(); 221 | const hasEPUB = result.files[language].EPUB; 222 | const issueFetch = { issueDate: issue, currentYear: +issue.substring(0, 4), language, hasEPUB: hasEPUB }; 223 | 224 | data = await fetchIssueData(issueFetch); 225 | } 226 | } 227 | 228 | return data; 229 | }; 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jw-epub-parser", 3 | "version": "3.37.2", 4 | "type": "module", 5 | "description": "This tool will help you to parse and extract the needed source materials from Meeting Workbook EPUB file. Support for parsing Watchtower Study will be added in future release.", 6 | "keywords": [ 7 | "epub parse", 8 | "jw", 9 | "jw.org", 10 | "epub" 11 | ], 12 | "main": "./dist/node/index.cjs", 13 | "module": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "files": [ 16 | "dist/*" 17 | ], 18 | "homepage": "https://github.com/sws2apps/jw-epub-parser#readme", 19 | "author": "Scheduling Workbox System ", 20 | "bugs": { 21 | "url": "https://github.com/sws2apps/jw-epub-parser/issues", 22 | "email": "sws2apps.notification@gmail.com" 23 | }, 24 | "license": "MIT", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/sws2apps/jw-epub-parser.git" 28 | }, 29 | "funding": { 30 | "type": "buymeacoffee", 31 | "url": "https://www.buymeacoffee.com/sws2apps" 32 | }, 33 | "scripts": { 34 | "test": "mocha", 35 | "build": "npx rimraf dist && npx rollup -c && npx tsup", 36 | "parse": "tsx example/index.ts", 37 | "setup:client": "cd client && npm install", 38 | "start:client": "cd client && npm run dev", 39 | "cypress:open": "cypress open", 40 | "cypress:run": "cypress run --browser chrome" 41 | }, 42 | "devDependencies": { 43 | "@babel/preset-env": "^7.16.11", 44 | "@rollup/plugin-babel": "^6.0.0", 45 | "@rollup/plugin-commonjs": "^28.0.0", 46 | "@rollup/plugin-json": "^6.0.0", 47 | "@rollup/plugin-node-resolve": "^16.0.0", 48 | "@rollup/plugin-terser": "^0.4.3", 49 | "@rollup/plugin-typescript": "^12.1.0", 50 | "@semantic-release/changelog": "^6.0.1", 51 | "@semantic-release/exec": "^7.0.2", 52 | "@semantic-release/git": "^10.0.1", 53 | "@types/node": "^24.0.0", 54 | "@types/path-browserify": "^1.0.2", 55 | "chai": "^5.0.0", 56 | "cypress": "^14.0.0", 57 | "mocha": "^11.0.1", 58 | "rimraf": "^6.0.0", 59 | "rollup": "^4.0.0", 60 | "semantic-release": "^24.0.0", 61 | "tsup": "^8.4.0", 62 | "tsx": "^4.7.0", 63 | "typescript": "^5.2.2" 64 | }, 65 | "dependencies": { 66 | "jszip": "^3.9.1", 67 | "node-html-parser": "^7.0.1", 68 | "path-browserify": "^1.0.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | 'main', 4 | { 5 | name: 'beta', 6 | prerelease: true, 7 | }, 8 | { 9 | name: 'alpha', 10 | prerelease: true, 11 | }, 12 | ], 13 | plugins: [ 14 | '@semantic-release/commit-analyzer', 15 | '@semantic-release/release-notes-generator', 16 | '@semantic-release/changelog', 17 | [ 18 | '@semantic-release/npm', 19 | { 20 | npmPublish: true, 21 | }, 22 | ], 23 | [ 24 | '@semantic-release/git', 25 | { 26 | assets: ['package.json', 'CHANGELOG.md'], 27 | message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 28 | }, 29 | ], 30 | [ 31 | '@semantic-release/exec', 32 | { 33 | prepareCmd: 'npm run build', 34 | }, 35 | ], 36 | [ 37 | '@semantic-release/github', 38 | { 39 | discussionCategoryName: 'Announcements', 40 | }, 41 | ], 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | import json from '@rollup/plugin-json'; 4 | import terser from '@rollup/plugin-terser'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | 7 | const config = [ 8 | { 9 | input: 'src/browser/index.ts', 10 | output: [ 11 | { 12 | sourcemap: true, 13 | file: 'dist/index.js', 14 | format: 'es', 15 | }, 16 | ], 17 | plugins: [ 18 | typescript({ 19 | tsconfig: './tsconfig.json', 20 | declaration: false, // Let tsup handle type declarations 21 | }), 22 | nodeResolve(), 23 | commonjs(), 24 | json(), 25 | terser(), 26 | ], 27 | external: ['jszip', 'path-browserify'], 28 | }, 29 | { 30 | input: 'src/node/index.ts', 31 | output: [ 32 | { 33 | sourcemap: false, 34 | file: 'dist/node/index.cjs', 35 | format: 'cjs', 36 | }, 37 | ], 38 | plugins: [ 39 | typescript({ 40 | tsconfig: './tsconfig.json', 41 | declaration: false, // Let tsup handle type declarations 42 | }), 43 | commonjs(), 44 | json(), 45 | terser(), 46 | ], 47 | external: ['jszip', 'node-html-parser', 'path', 'fs/promises'], 48 | }, 49 | { 50 | input: 'src/node/index.ts', 51 | output: [ 52 | { 53 | sourcemap: true, 54 | file: 'dist/node/index.js', 55 | format: 'es', 56 | }, 57 | ], 58 | plugins: [ 59 | typescript({ 60 | tsconfig: './tsconfig.json', 61 | declaration: false, // Let tsup handle type declarations 62 | }), 63 | commonjs(), 64 | json(), 65 | terser(), 66 | ], 67 | external: ['jszip', 'node-html-parser', 'path', 'fs/promises'], 68 | }, 69 | ]; 70 | 71 | export default config; 72 | -------------------------------------------------------------------------------- /src/browser/index.ts: -------------------------------------------------------------------------------- 1 | import '../browser/utils.browser.js'; 2 | import { getInputType, validateInput } from '../common/epub_validation.js'; 3 | import { startParse } from '../common/parser.js'; 4 | import { MWBSchedule, WSchedule } from '../types/index.js'; 5 | 6 | export type { MWBSchedule, WSchedule } 7 | 8 | export const loadEPUB = async (epubInput: File | Blob | { url: string }) => { 9 | try { 10 | validateInput(epubInput); 11 | 12 | // Step: Validate Environment 13 | const { browser } = getInputType(epubInput); 14 | if (!browser) { 15 | throw new Error( 16 | 'You are using the Browser version of jw-epub-parser. Please switch to the Node version if needed.' 17 | ); 18 | } 19 | 20 | // Step: Start Parsing 21 | const data = await startParse(epubInput); 22 | return data as any 23 | } catch (err) { 24 | console.error(err); 25 | 26 | throw new Error((err as Error)?.message); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/browser/utils.browser.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path-browserify'; 2 | 3 | import CH from '../locales/cmn-Hant/text.json'; 4 | import CHS from '../locales/ch-CHS/text.json'; 5 | import CR from '../locales/ht-HT/text.json'; 6 | import E from '../locales/en/text.json'; 7 | import ELI from '../locales/en-LR/text.json'; 8 | import F from '../locales/fr-FR/text.json'; 9 | import FI from '../locales/fi-FI/text.json'; 10 | import I from '../locales/it-IT/text.json'; 11 | import IL from '../locales/ilo-PH/text.json'; 12 | import J from '../locales/ja-JP/text.json'; 13 | import K from '../locales/uk-UA/text.json'; 14 | import KO from '../locales/ko-KR/text.json'; 15 | import M from '../locales/ro-RO/text.json'; 16 | import MG from '../locales/mg-MG/text.json'; 17 | import ML from '../locales/ms-MY/text.json'; 18 | import O from '../locales/nl-NL/text.json'; 19 | import P from '../locales/pl-PL/text.json'; 20 | import PGW from '../locales/wes-PGW/text.json'; 21 | import S from '../locales/es-ES/text.json'; 22 | import ST from '../locales/et-EE/text.json'; 23 | import SV from '../locales/sl-SI/text.json'; 24 | import SW from '../locales/sw-KE/text.json'; 25 | import T from '../locales/pt-POR/text.json'; 26 | import TG from '../locales/tl-PH/text.json'; 27 | import TK from '../locales/tr-TR/text.json'; 28 | import TND from '../locales/mg-TND/text.json'; 29 | import TNK from '../locales/mg-TNK/text.json'; 30 | import TPO from '../locales/pt-TPO/text.json'; 31 | import TTM from '../locales/mg-TTM/text.json'; 32 | import TW from '../locales/tw-TW/text.json'; 33 | import U from '../locales/ru-RU/text.json'; 34 | import VT from '../locales/vi-VN/text.json'; 35 | import VZ from '../locales/mg-VZ/text.json'; 36 | import X from '../locales/de-DE/text.json'; 37 | import YW from '../locales/rw-RW/text.json'; 38 | import Z from '../locales/sv-SE/text.json'; 39 | 40 | declare global { 41 | interface Window { 42 | jw_epub_parser: any; 43 | } 44 | } 45 | 46 | window.jw_epub_parser = { 47 | languages: { 48 | CH, 49 | CHS, 50 | CR, 51 | E, 52 | ELI, 53 | F, 54 | FI, 55 | I, 56 | IL, 57 | J, 58 | K, 59 | KO, 60 | M, 61 | MG, 62 | ML, 63 | O, 64 | P, 65 | PGW, 66 | S, 67 | ST, 68 | SV, 69 | SW, 70 | T, 71 | TG, 72 | TK, 73 | TND, 74 | TNK, 75 | TPO, 76 | TTM, 77 | TW, 78 | U, 79 | VT, 80 | VZ, 81 | X, 82 | YW, 83 | Z, 84 | }, 85 | path: path, 86 | }; 87 | -------------------------------------------------------------------------------- /src/classes/error.ts: -------------------------------------------------------------------------------- 1 | export class JWEPUBParserError extends Error { 2 | code: string; 3 | 4 | constructor(code: string, message: string) { 5 | super(message); 6 | 7 | this.code = `jw-epub-parser/failed-${code}`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/common/epub_jszip.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import { HTMLElement } from 'node-html-parser'; 3 | import { HTMLParse, getHTMLString, isValidHTML, isValidMWBSchedule, isValidWSchedule } from './html_validation.js'; 4 | 5 | export const extractEPUBFiles = async (data: string | ArrayBuffer | Buffer | Blob) => { 6 | const appZip = new JSZip(); 7 | 8 | const contents = await appZip.loadAsync(data); 9 | return contents; 10 | }; 11 | 12 | export const validateEPUBContents = async (data: string | ArrayBuffer | Buffer | Blob) => { 13 | const MAX_FILES = 300; 14 | const MAX_SIZE = 20000000; // 20 MO 15 | 16 | let fileCount = 0; 17 | let totalSize = 0; 18 | let targetDirectory = 'archive_tmp'; 19 | 20 | const result = { isBig: false, isMore: false, isSuspicious: false }; 21 | 22 | const appZip = new JSZip(); 23 | const contents = await appZip.loadAsync(data); 24 | 25 | for (let [filename] of Object.entries(contents.files)) { 26 | fileCount++; 27 | if (fileCount > MAX_FILES) { 28 | result.isMore = true; 29 | } 30 | 31 | // Prevent ZipSlip path traversal (S6096) 32 | const resolvedPath = jw_epub_parser.path.join(targetDirectory, filename); 33 | if (!resolvedPath.startsWith(targetDirectory)) { 34 | result.isSuspicious = true; 35 | } 36 | 37 | const contentSize = await appZip.file(filename)!.async('arraybuffer'); 38 | totalSize += contentSize.byteLength; 39 | if (totalSize > MAX_SIZE) { 40 | result.isBig = true; 41 | } 42 | } 43 | 44 | fileCount = 0; 45 | totalSize = 0; 46 | 47 | return result; 48 | }; 49 | 50 | export const getHTMLDocs = async (zip: JSZip, isMWB: boolean, isW: boolean) => { 51 | const files = []; 52 | 53 | for (let [filename] of Object.entries(zip.files)) { 54 | const isValidFile = isValidHTML(filename); 55 | 56 | if (isValidFile) { 57 | const content = await getHTMLString(zip, filename); 58 | const htmlDoc = HTMLParse(content); 59 | 60 | const isValidSchedule = isMWB ? isValidMWBSchedule(htmlDoc) : isW ? isValidWSchedule(htmlDoc) : false; 61 | 62 | if (isValidSchedule) { 63 | files.push(htmlDoc); 64 | } 65 | } 66 | } 67 | 68 | return files; 69 | }; 70 | 71 | export const getHTMLWTArticleDoc = async (zip: JSZip, articleFilename: string): Promise => { 72 | let article: HTMLElement; 73 | 74 | for (let [filename] of Object.entries(zip.files)) { 75 | const shortName = jw_epub_parser.path.basename(filename); 76 | if (shortName === articleFilename) { 77 | const content = await getHTMLString(zip, filename); 78 | const htmlDoc = HTMLParse(content); 79 | 80 | article = htmlDoc; 81 | break; 82 | } 83 | } 84 | 85 | return article!; 86 | }; 87 | -------------------------------------------------------------------------------- /src/common/epub_validation.ts: -------------------------------------------------------------------------------- 1 | export const isMWBEpub = (name: string) => { 2 | let regex = /^mwb_[A-Z][A-Z]?[A-Z]?_202\d(0[1-9]|1[0-2])\.epub$/i; 3 | return regex.test(name); 4 | }; 5 | 6 | export const isWEpub = (name: string) => { 7 | let regex = /^w_[A-Z][A-Z]?[A-Z]?_202\d(0[1-9]|1[0-2])\.epub$/i; 8 | return regex.test(name); 9 | }; 10 | 11 | export const validateInput = (input: string | { url: string } | Blob | File) => { 12 | if (!input) { 13 | throw new Error('You did not pass anything to the loadEPUB function.'); 14 | } 15 | }; 16 | 17 | export const getInputType = (input: string | { url: string } | Blob | File) => { 18 | const result = { browser: false, node: true }; 19 | 20 | if (typeof input === 'object' && 'url' in input) { 21 | result.browser = true; 22 | } else if (input instanceof Blob && 'name' in input) { 23 | result.browser = true; 24 | } 25 | 26 | return result; 27 | }; 28 | 29 | export const getEPUBFileName = (input: string | { url: string } | Blob) => { 30 | let filename: string; 31 | 32 | if (typeof input === 'object' && 'url' in input) { 33 | filename = input.url; 34 | } else if (input instanceof Blob && 'name' in input) { 35 | filename = input.name as string; 36 | } else { 37 | filename = input as string; 38 | } 39 | 40 | return jw_epub_parser.path.basename(filename); 41 | }; 42 | 43 | export const isValidEPUB = (input: string | { url: string } | Blob) => { 44 | const epubFilename = getEPUBFileName(input); 45 | const isMWB = isMWBEpub(epubFilename); 46 | const isW = isWEpub(epubFilename); 47 | 48 | return isMWB || isW; 49 | }; 50 | 51 | export const isValidEPUBIssue = (input: string | { url: string } | Blob) => { 52 | let valid = true; 53 | 54 | const epubFilename = getEPUBFileName(input); 55 | const isMWB = isMWBEpub(epubFilename); 56 | const isW = isWEpub(epubFilename); 57 | 58 | const type = isMWB ? 'mwb' : isW ? 'w' : undefined; 59 | 60 | const issue = +epubFilename.split('_')[2].split('.epub')[0]; 61 | 62 | if (type === 'mwb' && issue < 202207) valid = false; 63 | if (type === 'w' && issue < 202304) valid = false; 64 | 65 | return valid; 66 | }; 67 | 68 | export const getEPUBYear = (input: string | { url: string } | Blob) => { 69 | const filename = getEPUBFileName(input); 70 | return +filename.split('_')[2].substring(0, 4); 71 | }; 72 | 73 | export const getEPUBLanguage = (input: string | { url: string } | Blob) => { 74 | const filename = getEPUBFileName(input); 75 | return filename.split('_')[1]; 76 | }; 77 | 78 | export const getEPUBData = async (input: string | { url: string } | Blob) => { 79 | let result: Blob | ArrayBuffer | Buffer; 80 | 81 | if (input instanceof Blob) { 82 | result = input; 83 | } 84 | 85 | if (typeof input === 'object' && 'url' in input) { 86 | const epubRes = await fetch(input.url); 87 | if (epubRes.status !== 200) { 88 | throw new Error('EPUB file could not be downloaded. Check the URL you provided.'); 89 | } 90 | const epubData = await epubRes.blob(); 91 | const data = await epubData.arrayBuffer(); 92 | 93 | result = data; 94 | } 95 | 96 | if (typeof input === 'string') { 97 | const data = await jw_epub_parser.readFile(input); 98 | result = data; 99 | } 100 | 101 | return result!; 102 | }; 103 | -------------------------------------------------------------------------------- /src/common/html_utils.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import { HTMLElement } from 'node-html-parser'; 3 | import { getHTMLWTArticleDoc } from './epub_jszip.js'; 4 | import { extractSongNumber } from './parsing_rules.js'; 5 | 6 | export const getMWBWeekDate = (htmlItem: HTMLElement) => { 7 | const wdHtml = htmlItem.querySelector('h1')!; 8 | const weekDate = wdHtml.textContent.replace(/\u00A0/g, ' '); 9 | 10 | return weekDate; 11 | }; 12 | 13 | export const getMWBWeeklyBibleReading = (htmlItem: HTMLElement) => { 14 | const wbHtml = htmlItem.querySelector('h2')!; 15 | const weeklyBibleReading = wbHtml.textContent.replace(/\u00A0/g, ' '); 16 | 17 | return weeklyBibleReading; 18 | }; 19 | 20 | export const getMWBAYFCount = (htmlItem: HTMLElement) => { 21 | let count: number = 1; 22 | 23 | const testSection = htmlItem.querySelector('#section3'); 24 | 25 | // pre-2024 mwb 26 | if (testSection) { 27 | count = testSection.querySelectorAll('li').length; 28 | } 29 | 30 | // 2024 onward 31 | if (!testSection) { 32 | count = htmlItem.querySelectorAll('.du-color--gold-700').length - 1; 33 | } 34 | 35 | return count; 36 | }; 37 | 38 | export const getMWBLCCount = (htmlItem: HTMLElement) => { 39 | let count = 1; 40 | 41 | const testSection = htmlItem.querySelector('#section4'); 42 | 43 | // pre-2024 mwb 44 | if (testSection) { 45 | count = testSection.querySelectorAll('li').length; 46 | count = count === 6 ? 2 : 1; 47 | } 48 | 49 | // 2024 onward 50 | if (testSection === null) { 51 | count = htmlItem.querySelectorAll('.du-color--maroon-600.du-margin-top--8.du-margin-bottom--0').length - 1; 52 | } 53 | 54 | return count; 55 | }; 56 | 57 | export const getMWBSources = (htmlItem: HTMLElement) => { 58 | let src = ''; 59 | 60 | // pre-2024 mwb 61 | // get elements with meeting schedule data: pGroup 62 | const pGroupData = htmlItem.querySelectorAll('.pGroup'); 63 | for (const pGroup of pGroupData) { 64 | const liData = pGroup.querySelectorAll('li'); 65 | for (const li of liData) { 66 | const firstP = li.querySelector('p')!; 67 | src += '@' + firstP.textContent; 68 | } 69 | } 70 | 71 | // 2024 onward 72 | // get elements with meeting schedule data: h3 73 | if (src.length === 0) { 74 | const h3Texts = htmlItem.querySelectorAll('h3'); 75 | 76 | let songIndex = 0; 77 | 78 | for (const h3 of h3Texts) { 79 | let isSong = h3.classList.contains('dc-icon--music'); 80 | const part = h3.parentNode.classList.contains('boxContent') === false; 81 | 82 | if (!isSong) { 83 | isSong = h3.querySelector('.dc-icon--music') ? true : false; 84 | } 85 | 86 | if (isSong) { 87 | songIndex++; 88 | } 89 | 90 | if (isSong || part) { 91 | let data = ''; 92 | 93 | data = h3.textContent; 94 | 95 | if (isSong) { 96 | data = data.replace('|', '@'); 97 | } 98 | 99 | src += '@' + data; 100 | 101 | const nextSibling = h3.nextElementSibling; 102 | 103 | if (nextSibling) { 104 | const nextElement = nextSibling.querySelector('p'); 105 | 106 | if (nextElement) { 107 | // handle element exception in mwb25.09 108 | if (isSong && songIndex === 2 && nextSibling.tagName === 'DIV') { 109 | src += '@' + nextElement.textContent; 110 | 111 | const tmpSibling = nextSibling.nextElementSibling?.querySelector('p'); 112 | 113 | if (tmpSibling) { 114 | src += ' ' + tmpSibling.textContent; 115 | } 116 | 117 | continue; 118 | } 119 | 120 | src += ' ' + nextElement.textContent; 121 | } 122 | } 123 | } 124 | } 125 | 126 | const sepBeforeBR = src.split('@', 5).join('@').length; 127 | src = src.substring(0, sepBeforeBR) + '@junk@junk' + src.substring(sepBeforeBR); 128 | } 129 | 130 | src = src.replace(/\u00A0/g, ' '); // remove non-breaking space 131 | 132 | return src; 133 | }; 134 | 135 | export const getWStudyArticles = (htmlItem: HTMLElement) => { 136 | return htmlItem.querySelectorAll('h3'); 137 | }; 138 | 139 | export const getWStudyDate = (htmlItem: HTMLElement) => { 140 | let result: string; 141 | 142 | const p = htmlItem.querySelector('.desc'); 143 | 144 | if (p === null) { 145 | result = htmlItem.textContent.replace(/\u00A0/g, ' '); // remove non-breaking space; 146 | } 147 | 148 | if (p !== null) { 149 | result = p.textContent.replace(/\u00A0/g, ' '); // remove non-breaking space; 150 | } 151 | 152 | return result!; 153 | }; 154 | 155 | export const getWSTudySongs = (content: HTMLElement) => { 156 | const pubRefs = content.querySelectorAll('.pubRefs'); 157 | 158 | const openingSongText = pubRefs.at(0)!; 159 | const w_study_opening_song = extractSongNumber(openingSongText.textContent) as number; 160 | 161 | let concludingSongText = pubRefs.at(-1); 162 | 163 | if (pubRefs.length === 2) { 164 | const blockTeach = content.querySelector('.blockTeach'); 165 | concludingSongText = blockTeach!.nextElementSibling!; 166 | } 167 | 168 | const w_study_concluding_song = extractSongNumber(concludingSongText.textContent) as number; 169 | 170 | return { 171 | w_study_opening_song, 172 | w_study_concluding_song, 173 | }; 174 | }; 175 | 176 | export const getWStudyTitle = (htmlItem: HTMLElement) => { 177 | let result: string; 178 | 179 | const h2 = htmlItem.querySelector('h2'); 180 | 181 | if (h2 === null) { 182 | const articleLink = htmlItem.nextElementSibling!.querySelector('a')!; 183 | result = articleLink.textContent.replace(/\u00A0/g, ' '); // remove non-breaking space;; 184 | } 185 | 186 | if (h2 !== null) { 187 | result = h2.textContent.trim().replace(/\u00A0/g, ' '); // remove non-breaking space; 188 | } 189 | 190 | return result!; 191 | }; 192 | -------------------------------------------------------------------------------- /src/common/html_validation.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import { HTMLElement, parse } from 'node-html-parser'; 3 | 4 | export const isValidHTML = (name: string): boolean => { 5 | let valid = false; 6 | 7 | if (name.startsWith('OEBPS') && name.endsWith('.xhtml') && name.indexOf('-extracted') === -1) { 8 | const fileName = name.split('/')[1].split('.')[0]; 9 | if (!isNaN(parseFloat(fileName))) { 10 | valid = true; 11 | } 12 | } 13 | 14 | return valid; 15 | }; 16 | 17 | export const getHTMLString = async (zip: JSZip, filename: string): Promise => { 18 | const content = await zip.file(filename)!.async('string'); 19 | return content; 20 | }; 21 | 22 | export const isValidMWBSchedule = (htmlDoc: HTMLElement): boolean => { 23 | let valid = false; 24 | let isValidTGW = false; 25 | let isValidAYF = false; 26 | let isValidLC = false; 27 | 28 | // pre-2024 mwb 29 | 30 | isValidTGW = htmlDoc.querySelector(`[class*=treasures]`) ? true : false; 31 | if (isValidTGW) { 32 | isValidAYF = htmlDoc.querySelector(`[class*=ministry]`) ? true : false; 33 | isValidLC = htmlDoc.querySelector(`[class*=christianLiving]`) ? true : false; 34 | } 35 | 36 | // 2024 onward 37 | if (!isValidTGW) { 38 | isValidTGW = htmlDoc.querySelector('.du-color--teal-700') ? true : false; 39 | if (isValidTGW) { 40 | isValidAYF = htmlDoc.querySelector('.du-color--gold-700') ? true : false; 41 | isValidLC = htmlDoc.querySelector('.du-color--maroon-600') ? true : false; 42 | } 43 | } 44 | 45 | if (isValidTGW === true && isValidAYF === true && isValidLC === true) { 46 | valid = true; 47 | } 48 | 49 | return valid; 50 | }; 51 | 52 | export const HTMLParse = (htmlString: string): HTMLElement => { 53 | const htmlDoc = parse(htmlString); 54 | 55 | return htmlDoc; 56 | }; 57 | 58 | export const isValidWSchedule = (htmlDoc: HTMLElement): boolean => { 59 | const valid = htmlDoc.querySelector('.groupTOC') ? true : false; 60 | return valid; 61 | }; 62 | -------------------------------------------------------------------------------- /src/common/language_rules.ts: -------------------------------------------------------------------------------- 1 | import { Language } from '../types/index.js'; 2 | 3 | const languages: Language = jw_epub_parser.languages; 4 | 5 | export const getMonthNames = (lang: string) => { 6 | return [ 7 | { index: 0, name: languages[lang].januaryVariations }, 8 | { index: 1, name: languages[lang].februaryVariations }, 9 | { index: 2, name: languages[lang].marchVariations }, 10 | { index: 3, name: languages[lang].aprilVariations }, 11 | { index: 4, name: languages[lang].mayVariations }, 12 | { index: 5, name: languages[lang].juneVariations }, 13 | { index: 6, name: languages[lang].julyVariations }, 14 | { index: 7, name: languages[lang].augustVariations }, 15 | { index: 8, name: languages[lang].septemberVariations }, 16 | { index: 9, name: languages[lang].octoberVariations }, 17 | { index: 10, name: languages[lang].novemberVariations }, 18 | { index: 11, name: languages[lang].decemberVariations }, 19 | ]; 20 | }; 21 | 22 | export const getPartMinutesSeparatorVariations = (lang: string) => languages[lang].partMinutesSeparatorVariations; 23 | -------------------------------------------------------------------------------- /src/common/override.ts: -------------------------------------------------------------------------------- 1 | import { Override } from '../types'; 2 | 3 | const overrides: Override = { 4 | ELI: { 5 | 'Study Article 23: July 31–August 6 20': 'Study Article 23: July 31, 2023–August 6, 2023', 6 | }, 7 | PGW: { 8 | 'Fine-Fine Things Wey You See for Bible': 'Fine-Fine Things Wey You See for Bible: (10 min.)', 9 | }, 10 | ST: { 11 | 'Seda artiklit uuritakse vahemikus 28. oktoobrist 3.novembrini 2024.': 12 | 'Seda artiklit uuritakse vahemikus 28. oktoobrist 3. novembrini 2024.', 13 | }, 14 | SW: { 15 | '7. Video ya Machi ya Mambo Yaliyotimizwa na Tengenezo (10 min.) Onyesha VIDEO.': 16 | '7. Video ya Machi ya Mambo Yaliyotimizwa na Tengenezo (Dak. 10) Onyesha VIDEO.', 17 | }, 18 | YW: { 19 | '4. Gutangiza ikiganiro (Umun. 1) KUBWIRIZA KU NZU N’INZU. Umubwiriza asange umuntu avuga urundi rurimi. (lmd isomo rya 2 ingingo ya 5)': 20 | '4. Gutangiza ikiganiro (Imin. 1) KUBWIRIZA KU NZU N’INZU. Umubwiriza asange umuntu avuga urundi rurimi. (lmd isomo rya 2 ingingo ya 5)', 21 | }, 22 | }; 23 | 24 | export default overrides; 25 | -------------------------------------------------------------------------------- /src/common/parser.ts: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip'; 2 | import { HTMLElement } from 'node-html-parser'; 3 | import languages from '../locales/languages.js'; 4 | import { extractEPUBFiles, getHTMLDocs, getHTMLWTArticleDoc, validateEPUBContents } from './epub_jszip.js'; 5 | import { 6 | getEPUBData, 7 | getEPUBFileName, 8 | getEPUBLanguage, 9 | getEPUBYear, 10 | isMWBEpub, 11 | isValidEPUB, 12 | isValidEPUBIssue, 13 | isWEpub, 14 | } from './epub_validation.js'; 15 | import { 16 | getMWBAYFCount, 17 | getMWBLCCount, 18 | getMWBSources, 19 | getMWBWeekDate, 20 | getMWBWeeklyBibleReading, 21 | getWSTudySongs, 22 | getWStudyArticles, 23 | getWStudyDate, 24 | getWStudyTitle, 25 | } from './html_utils.js'; 26 | import { extractSongNumber, extractSourceEnhanced } from './parsing_rules.js'; 27 | import { MWBSchedule, WSchedule } from '../types/index.js'; 28 | import { extractMWBDate, extractWTStudyDate } from './date_parser.js'; 29 | 30 | export const startParse = async (epubInput: string | Blob | { url: string }) => { 31 | let result = {}; 32 | 33 | const isValidName = isValidEPUB(epubInput); 34 | if (!isValidName) { 35 | throw new Error('The selected epub file has an incorrect naming.'); 36 | } 37 | 38 | const isValiIssue = isValidEPUBIssue(epubInput); 39 | if (!isValiIssue) { 40 | throw new Error( 41 | 'EPUB import is only supported for Meeting Workbook starting on July 2022, and for Watchtower Study starting on April 2023.' 42 | ); 43 | } 44 | 45 | const epubBuffer = await getEPUBData(epubInput); 46 | const epubCheck = await validateEPUBContents(epubBuffer); 47 | 48 | if (epubCheck.isBig) { 49 | throw new Error('EPUB file seems to be large. Extract aborted.'); 50 | } 51 | 52 | if (epubCheck.isMore) { 53 | throw new Error('EPUB file seems to contain more files than expected. Extract aborted.'); 54 | } 55 | 56 | if (epubCheck.isSuspicious) { 57 | throw new Error('EPUB file seems to be suspicious. Extract aborted.'); 58 | } 59 | 60 | const epubFilename = getEPUBFileName(epubInput); 61 | const isMWB = isMWBEpub(epubFilename); 62 | const isW = isWEpub(epubFilename); 63 | 64 | const epubContents = await extractEPUBFiles(epubBuffer); 65 | 66 | const htmlDocs = await getHTMLDocs(epubContents, isMWB, isW); 67 | 68 | if (htmlDocs.length === 0) { 69 | throw new Error( 70 | `The file you provided is not a valid ${ 71 | isMWB ? 'Meeting Workbook' : 'Watchtower Study' 72 | } EPUB file. Please make sure that the file is correct.` 73 | ); 74 | } 75 | 76 | if (isW && htmlDocs.length > 1) { 77 | throw new Error( 78 | `The file you provided is not a valid Watchtower Study EPUB file. Please make sure that the file is correct.` 79 | ); 80 | } 81 | 82 | const epubYear = getEPUBYear(epubInput); 83 | const epubLang = getEPUBLanguage(epubInput); 84 | 85 | if (isMWB) { 86 | result = await parseMWBEpub({ htmlDocs, epubYear, epubLang }); 87 | } 88 | 89 | if (isW) { 90 | result = await parseWEpub({ 91 | htmlItem: htmlDocs[0], 92 | epubLang, 93 | epubContents, 94 | }); 95 | } 96 | 97 | return result; 98 | }; 99 | 100 | export const parseMWBSchedule = (htmlItem: HTMLElement, mwbYear: number, mwbLang: string) => { 101 | const isEnhancedParsing = languages.find((language) => language.code === mwbLang); 102 | 103 | const weekItem = {} as MWBSchedule; 104 | 105 | // get week date 106 | const weekDate = getMWBWeekDate(htmlItem); 107 | 108 | if (isEnhancedParsing) { 109 | const weekDateEnhanced = extractMWBDate(weekDate, mwbYear, mwbLang); 110 | weekItem.mwb_week_date = weekDateEnhanced; 111 | weekItem.mwb_week_date_locale = weekDate; 112 | } else { 113 | weekItem.mwb_week_date = weekDate; 114 | } 115 | 116 | // get weekly Bible Reading 117 | weekItem.mwb_weekly_bible_reading = getMWBWeeklyBibleReading(htmlItem); 118 | 119 | // compile all sources 120 | const src = getMWBSources(htmlItem); 121 | let splits = src.split('@'); 122 | 123 | let tmpSrc = ''; 124 | 125 | // First song 126 | weekItem.mwb_song_first = extractSongNumber(splits[1]) as number; 127 | 128 | // 10min TGW Source 129 | tmpSrc = splits[3].trim(); 130 | if (isEnhancedParsing) { 131 | const enhanced = extractSourceEnhanced(tmpSrc, mwbLang); 132 | weekItem.mwb_tgw_talk = enhanced.type; 133 | weekItem.mwb_tgw_talk_title = enhanced.fulltitle; 134 | } else { 135 | weekItem.mwb_tgw_talk = tmpSrc; 136 | } 137 | 138 | // 10min Spiritual Gems 139 | tmpSrc = splits[4].trim(); 140 | if (isEnhancedParsing) { 141 | const enhanced = extractSourceEnhanced(tmpSrc, mwbLang); 142 | weekItem.mwb_tgw_gems_title = enhanced.fulltitle; 143 | } else { 144 | weekItem.mwb_tgw_gems_title = tmpSrc; 145 | } 146 | 147 | //Bible Reading Source 148 | tmpSrc = splits[7].trim(); 149 | if (isEnhancedParsing) { 150 | const enhanced = extractSourceEnhanced(tmpSrc, mwbLang); 151 | weekItem.mwb_tgw_bread = enhanced.src!; 152 | weekItem.mwb_tgw_bread_title = enhanced.fulltitle; 153 | } else { 154 | weekItem.mwb_tgw_bread = tmpSrc; 155 | } 156 | 157 | // get number of assignments in Apply Yourself Parts 158 | const cnAYF = getMWBAYFCount(htmlItem); 159 | 160 | // AYF Part Count 161 | weekItem.mwb_ayf_count = cnAYF; 162 | 163 | //AYF1 Source 164 | tmpSrc = splits[8].trim(); 165 | if (isEnhancedParsing) { 166 | const partEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 167 | weekItem.mwb_ayf_part1 = partEnhanced.src!; 168 | weekItem.mwb_ayf_part1_time = partEnhanced.time; 169 | weekItem.mwb_ayf_part1_type = partEnhanced.type; 170 | weekItem.mwb_ayf_part1_title = partEnhanced.fulltitle; 171 | } else { 172 | weekItem.mwb_ayf_part1 = tmpSrc; 173 | } 174 | 175 | //AYF2 Source 176 | if (cnAYF > 1) { 177 | tmpSrc = splits[9].trim(); 178 | if (isEnhancedParsing) { 179 | const partEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 180 | weekItem.mwb_ayf_part2 = partEnhanced.src!; 181 | weekItem.mwb_ayf_part2_time = partEnhanced.time; 182 | weekItem.mwb_ayf_part2_type = partEnhanced.type; 183 | weekItem.mwb_ayf_part2_title = partEnhanced.fulltitle; 184 | } else { 185 | weekItem.mwb_ayf_part2 = tmpSrc; 186 | } 187 | } 188 | 189 | //AYF3 Source 190 | if (cnAYF > 2) { 191 | tmpSrc = splits[10].trim(); 192 | if (isEnhancedParsing) { 193 | const partEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 194 | weekItem.mwb_ayf_part3 = partEnhanced.src!; 195 | weekItem.mwb_ayf_part3_time = partEnhanced.time; 196 | weekItem.mwb_ayf_part3_type = partEnhanced.type; 197 | weekItem.mwb_ayf_part3_title = partEnhanced.fulltitle; 198 | } else { 199 | weekItem.mwb_ayf_part3 = tmpSrc; 200 | } 201 | } 202 | 203 | // AYF4 Source 204 | if (cnAYF > 3) { 205 | tmpSrc = splits[11].trim(); 206 | if (isEnhancedParsing) { 207 | const partEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 208 | weekItem.mwb_ayf_part4 = partEnhanced.src; 209 | weekItem.mwb_ayf_part4_time = partEnhanced.time; 210 | weekItem.mwb_ayf_part4_type = partEnhanced.type; 211 | weekItem.mwb_ayf_part4_title = partEnhanced.fulltitle; 212 | } else { 213 | weekItem.mwb_ayf_part4 = tmpSrc; 214 | } 215 | } 216 | 217 | // Middle song 218 | let nextIndex = cnAYF > 3 ? 12 : cnAYF > 2 ? 11 : cnAYF > 1 ? 10 : 9; 219 | weekItem.mwb_song_middle = extractSongNumber(splits[nextIndex]); 220 | 221 | // get number of assignments in Living as Christians Parts 222 | const cnLC = getMWBLCCount(htmlItem); 223 | 224 | // LC Part Count 225 | weekItem.mwb_lc_count = cnLC; 226 | 227 | // 1st LC part 228 | nextIndex++; 229 | 230 | tmpSrc = splits[nextIndex].trim(); 231 | if (isEnhancedParsing) { 232 | const lcEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 233 | weekItem.mwb_lc_part1 = lcEnhanced.type; 234 | weekItem.mwb_lc_part1_time = lcEnhanced.time; 235 | weekItem.mwb_lc_part1_title = lcEnhanced.fulltitle; 236 | if (lcEnhanced.src && lcEnhanced.src !== '') { 237 | weekItem.mwb_lc_part1_content = lcEnhanced.src; 238 | } 239 | } else { 240 | weekItem.mwb_lc_part1 = tmpSrc; 241 | } 242 | 243 | // 2nd LC part 244 | if (cnLC === 2) { 245 | nextIndex++; 246 | tmpSrc = splits[nextIndex].trim(); 247 | 248 | if (isEnhancedParsing) { 249 | const lcEnhanced = extractSourceEnhanced(tmpSrc, mwbLang); 250 | weekItem.mwb_lc_part2 = lcEnhanced.type; 251 | weekItem.mwb_lc_part2_time = lcEnhanced.time; 252 | weekItem.mwb_lc_part2_title = lcEnhanced.fulltitle; 253 | if (lcEnhanced.src && lcEnhanced.src !== '') { 254 | weekItem.mwb_lc_part2_content = lcEnhanced.src; 255 | } 256 | } else { 257 | weekItem.mwb_lc_part2 = tmpSrc; 258 | } 259 | } 260 | 261 | // CBS Source 262 | nextIndex++; 263 | tmpSrc = splits[nextIndex].trim(); 264 | 265 | if (isEnhancedParsing) { 266 | const enhanced = extractSourceEnhanced(tmpSrc, mwbLang); 267 | weekItem.mwb_lc_cbs = enhanced.src!; 268 | weekItem.mwb_lc_cbs_title = enhanced.fulltitle; 269 | } else { 270 | weekItem.mwb_lc_cbs = tmpSrc; 271 | } 272 | 273 | // Concluding Song 274 | nextIndex++; 275 | nextIndex++; 276 | tmpSrc = splits[nextIndex].trim(); 277 | weekItem.mwb_song_conclude = extractSongNumber(tmpSrc); 278 | 279 | return weekItem; 280 | }; 281 | 282 | export const parseWSchedule = (article: HTMLElement, content: HTMLElement, wLang: string) => { 283 | const isEnhancedParsing = languages.find((language) => language.code === wLang); 284 | 285 | const weekItem = {} as WSchedule; 286 | 287 | const studyDate = getWStudyDate(article); 288 | 289 | if (studyDate.length > 0) { 290 | if (isEnhancedParsing) { 291 | const wStudyEnhanced = extractWTStudyDate(studyDate, wLang); 292 | weekItem.w_study_date = wStudyEnhanced; 293 | weekItem.w_study_date_locale = studyDate; 294 | } else { 295 | weekItem.w_study_date = studyDate; 296 | } 297 | } 298 | 299 | const studyTitle = getWStudyTitle(article); 300 | weekItem.w_study_title = studyTitle; 301 | 302 | const songs = getWSTudySongs(content); 303 | 304 | weekItem.w_study_opening_song = songs.w_study_opening_song; 305 | weekItem.w_study_concluding_song = songs.w_study_concluding_song; 306 | 307 | return weekItem; 308 | }; 309 | 310 | const parseMWBEpub = async ({ 311 | htmlDocs, 312 | epubYear, 313 | epubLang, 314 | }: { 315 | htmlDocs: HTMLElement[]; 316 | epubYear: number; 317 | epubLang: string; 318 | }) => { 319 | const weeksData = []; 320 | 321 | for (const htmlItem of htmlDocs) { 322 | const weekItem = parseMWBSchedule(htmlItem, epubYear, epubLang); 323 | weeksData.push(weekItem); 324 | } 325 | 326 | return weeksData; 327 | }; 328 | 329 | const parseWEpub = async ({ 330 | htmlItem, 331 | epubLang, 332 | epubContents, 333 | }: { 334 | htmlItem: HTMLElement; 335 | epubLang: string; 336 | epubContents: JSZip; 337 | }) => { 338 | const weeksData = []; 339 | 340 | const studyArticles = getWStudyArticles(htmlItem); 341 | 342 | for (const [_, studyArticle] of studyArticles.entries()) { 343 | const articleLink = studyArticle.nextElementSibling!.querySelector('a')!.getAttribute('href') as string; 344 | const content = await getHTMLWTArticleDoc(epubContents, articleLink); 345 | 346 | const weekItem = parseWSchedule(studyArticle, content, epubLang); 347 | weeksData.push(weekItem); 348 | } 349 | 350 | return weeksData; 351 | }; 352 | -------------------------------------------------------------------------------- /src/common/parsing_rules.ts: -------------------------------------------------------------------------------- 1 | import { JWEPUBParserError } from '../classes/error.js'; 2 | import { LangRegExp } from '../types/index.js'; 3 | import { getPartMinutesSeparatorVariations } from './language_rules.js'; 4 | import overrides from './override.js'; 5 | 6 | export const extractSongNumber = (src: string) => { 7 | const parseNum = src.match(/(\d+)/); 8 | 9 | if (parseNum && parseNum.length > 0) { 10 | const firstNumber = +parseNum[0]; 11 | 12 | if (firstNumber <= 161) { 13 | return firstNumber; 14 | } 15 | } 16 | 17 | return src; 18 | }; 19 | 20 | export const extractSourceEnhanced = (src: string, lang: string) => { 21 | const variations = getPartMinutesSeparatorVariations(lang); 22 | 23 | let finalSrc = src; 24 | 25 | const overrideLang = overrides[lang]; 26 | 27 | if (overrideLang) { 28 | const overrideSrc = overrideLang[src]; 29 | 30 | if (overrideSrc) { 31 | finalSrc = overrideSrc; 32 | } 33 | } 34 | 35 | // separate minutes from title 36 | const firstPatternCommon = new RegExp( 37 | `(.+?)(?:: )?[((](\\d+)(?: | | )?(?:${variations})[))](?: : | |. )?(.+?)?$`, 38 | 'giu' 39 | ); 40 | 41 | const firstPatternCommonPGW = new RegExp( 42 | `(.+?)(?:: )?[((](\\d+)(?: | )?(?:${variations})[))]?(?: : | |. )?(.+?)?$`, 43 | 'giu' 44 | ); 45 | 46 | const firstPatternTW = new RegExp(`(.+?)(?: )?\\((?:${variations})(?: | )?(\\d+).?\\)(?: |.)?(.+?)?$`, 'giu'); 47 | 48 | const firstPattern: LangRegExp = { 49 | common: firstPatternCommon, 50 | PGW: firstPatternCommonPGW, 51 | SW: firstPatternTW, 52 | TW: firstPatternTW, 53 | YW: firstPatternTW, 54 | }; 55 | 56 | const langPattern = firstPattern[lang] || firstPattern.common; 57 | 58 | const matchFirstPattern = finalSrc.match(langPattern); 59 | 60 | if (!matchFirstPattern) { 61 | throw new JWEPUBParserError('jw-epub-parser', `Parsing failed. The input was: ${finalSrc}`); 62 | } 63 | 64 | const groupsFirstPattern = Array.from(langPattern.exec(finalSrc)!); 65 | 66 | const fulltitle = groupsFirstPattern.at(1)!.trim(); 67 | const time = +groupsFirstPattern.at(2)!.trim(); 68 | const source = groupsFirstPattern.at(3)?.trim(); 69 | 70 | // separate index from title 71 | const nextPattern = /^(:?\d+)(?:.|.\s)(.+?)$/giu; 72 | 73 | const matchNextPattern = fulltitle.match(nextPattern); 74 | 75 | let type = fulltitle; 76 | 77 | if (matchNextPattern) { 78 | const groupsNextPattern = Array.from(nextPattern.exec(fulltitle)!); 79 | type = groupsNextPattern.at(2)!.trim(); 80 | } 81 | 82 | return { type, src: source, time, fulltitle }; 83 | }; 84 | -------------------------------------------------------------------------------- /src/locales/ch-CHS/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "一月", 3 | "februaryVariations": "二月", 4 | "marchVariations": "三月", 5 | "aprilVariations": "四月", 6 | "mayVariations": "五月", 7 | "juneVariations": "六月", 8 | "julyVariations": "七月", 9 | "augustVariations": "八月", 10 | "septemberVariations": "九月", 11 | "octoberVariations": "十月", 12 | "novemberVariations": "十一月", 13 | "decemberVariations": "十二月", 14 | "partMinutesSeparatorVariations": "分钟" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/cmn-Hant/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "1月", 3 | "februaryVariations": "2月", 4 | "marchVariations": "3月", 5 | "aprilVariations": "4 月", 6 | "mayVariations": "5 月", 7 | "juneVariations": "6 月", 8 | "julyVariations": "7 月", 9 | "augustVariations": "8 月", 10 | "septemberVariations": "9 月", 11 | "octoberVariations": "10 月", 12 | "novemberVariations": "11 月", 13 | "decemberVariations": "12 月", 14 | "partMinutesSeparatorVariations": "分鐘" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/de-DE/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Januar", 3 | "februaryVariations": "Februar", 4 | "marchVariations": "März", 5 | "aprilVariations": "April", 6 | "mayVariations": "Mai", 7 | "juneVariations": "Juni", 8 | "julyVariations": "Juli", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "Oktober", 12 | "novemberVariations": "November", 13 | "decemberVariations": "Dezember", 14 | "partMinutesSeparatorVariations": "Min|Min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/en-LR/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/en/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/es-ES/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Enero", 3 | "februaryVariations": "Febrero", 4 | "marchVariations": "Marzo", 5 | "aprilVariations": "Abril", 6 | "mayVariations": "Mayo", 7 | "juneVariations": "Junio", 8 | "julyVariations": "Julio", 9 | "augustVariations": "Agosto", 10 | "septemberVariations": "Septiembre", 11 | "octoberVariations": "Octubre", 12 | "novemberVariations": "Noviembre", 13 | "decemberVariations": "Diciembre", 14 | "partMinutesSeparatorVariations": "mins.|min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/es-LSE/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Enero", 3 | "februaryVariations": "Febrero", 4 | "marchVariations": "Marzo", 5 | "aprilVariations": "Abril", 6 | "mayVariations": "Mayo", 7 | "juneVariations": "Junio", 8 | "julyVariations": "Julio", 9 | "augustVariations": "Agosto", 10 | "septemberVariations": "Septiembre", 11 | "octoberVariations": "Octubre", 12 | "novemberVariations": "Noviembre", 13 | "decemberVariations": "Diciembre", 14 | "partMinutesSeparatorVariations": "mins.|min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/et-EE/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Jaanuar|jaanuarini|jaanuarist", 3 | "februaryVariations": "Veebruar|veebruarini|veebruarist", 4 | "marchVariations": "Märts|märtsini|märtsist", 5 | "aprilVariations": "Aprill|aprillini|aprillist", 6 | "mayVariations": "Mai|maini|maist", 7 | "juneVariations": "Juuni|juunini", 8 | "julyVariations": "Juuli|juulini|juulist", 9 | "augustVariations": "August|augustini|augustist", 10 | "septemberVariations": "September|septembrini|septembrist", 11 | "octoberVariations": "Oktoober|oktoobrini|oktoobrist", 12 | "novemberVariations": "November|novembrini|novembrist", 13 | "decemberVariations": "Detsember|detsembrini|detsembrist", 14 | "partMinutesSeparatorVariations": "min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/fi-FI/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "tammikuuta", 3 | "februaryVariations": "helmikuuta", 4 | "marchVariations": "maaliskuuta", 5 | "aprilVariations": "huhtikuuta", 6 | "mayVariations": "toukokuuta", 7 | "juneVariations": "kesäkuuta", 8 | "julyVariations": "heinäkuuta", 9 | "augustVariations": "elokuuta", 10 | "septemberVariations": "syyskuuta", 11 | "octoberVariations": "lokakuuta", 12 | "novemberVariations": "marraskuuta", 13 | "decemberVariations": "joulukuuta", 14 | "partMinutesSeparatorVariations": "min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/fr-FR/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "janvier", 3 | "februaryVariations": "février", 4 | "marchVariations": "mars", 5 | "aprilVariations": "avril", 6 | "mayVariations": "mai", 7 | "juneVariations": "juin", 8 | "julyVariations": "juillet", 9 | "augustVariations": "août", 10 | "septemberVariations": "septembre", 11 | "octoberVariations": "octobre", 12 | "novemberVariations": "novembre", 13 | "decemberVariations": "décembre", 14 | "partMinutesSeparatorVariations": "min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ht-HT/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Janvye", 3 | "februaryVariations": "Fevriye", 4 | "marchVariations": "Mas", 5 | "aprilVariations": "Avril", 6 | "mayVariations": "Me", 7 | "juneVariations": "Jen", 8 | "julyVariations": "Jiyè", 9 | "augustVariations": "Out", 10 | "septemberVariations": "Septanm", 11 | "octoberVariations": "Oktòb", 12 | "novemberVariations": "Novanm", 13 | "decemberVariations": "Desanm", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/hu-HU/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Január", 3 | "februaryVariations": "Február", 4 | "marchVariations": "Március", 5 | "aprilVariations": "Április", 6 | "mayVariations": "Május", 7 | "juneVariations": "Június", 8 | "julyVariations": "Július", 9 | "augustVariations": "Augusztus", 10 | "septemberVariations": "Szeptember", 11 | "octoberVariations": "Október", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "perc" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ilo-PH/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Enero", 3 | "februaryVariations": "Pebrero", 4 | "marchVariations": "Marso", 5 | "aprilVariations": "Abril", 6 | "mayVariations": "Mayo", 7 | "juneVariations": "Hunio", 8 | "julyVariations": "Hulio", 9 | "augustVariations": "Agosto", 10 | "septemberVariations": "Septiembre", 11 | "octoberVariations": "Oktubre", 12 | "novemberVariations": "Nobyembre|Nobiembre", 13 | "decemberVariations": "Disiembre", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/it-IT/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Gennaio", 3 | "februaryVariations": "Febbraio", 4 | "marchVariations": "Marzo", 5 | "aprilVariations": "Aprile", 6 | "mayVariations": "Maggio", 7 | "juneVariations": "Giugno", 8 | "julyVariations": "Luglio", 9 | "augustVariations": "Agosto", 10 | "septemberVariations": "Settembre", 11 | "octoberVariations": "Ottobre", 12 | "novemberVariations": "Novembre", 13 | "decemberVariations": "Dicembre", 14 | "partMinutesSeparatorVariations": "min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ja-JP/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "1月", 3 | "februaryVariations": "2月", 4 | "marchVariations": "3月", 5 | "aprilVariations": "4月", 6 | "mayVariations": "5月", 7 | "juneVariations": "6月", 8 | "julyVariations": "7月", 9 | "augustVariations": "8月", 10 | "septemberVariations": "9月", 11 | "octoberVariations": "10月", 12 | "novemberVariations": "11月", 13 | "decemberVariations": "12月", 14 | "partMinutesSeparatorVariations": "分" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ko-KR/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "분" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/languages.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { locale: 'en', code: 'E' }, 3 | { locale: 'fr-FR', code: 'F' }, 4 | { locale: 'it-IT', code: 'I' }, 5 | { locale: 'ja-JP', code: 'J' }, 6 | { locale: 'uk-UA', code: 'K' }, 7 | { locale: 'ko-KR', code: 'KO' }, 8 | { locale: 'ro-RO', code: 'M' }, 9 | { locale: 'mg-MG', code: 'MG' }, 10 | { locale: 'pl-PL', code: 'P' }, 11 | { locale: 'es-ES', code: 'S' }, 12 | { locale: 'pt-POR', code: 'T' }, 13 | { locale: 'pt-TPO', code: 'TPO' }, 14 | { locale: 'tl-PH', code: 'TG' }, 15 | { locale: 'tr-TR', code: 'TK' }, 16 | { locale: 'mg-TND', code: 'TND' }, 17 | { locale: 'mg-TNK', code: 'TNK' }, 18 | { locale: 'mg-TTM', code: 'TTM' }, 19 | { locale: 'tw-TW', code: 'TW' }, 20 | { locale: 'uk-UA', code: 'U' }, 21 | { locale: 'mg-VZ', code: 'VZ' }, 22 | { locale: 'de-DE', code: 'X' }, 23 | { locale: 'cmn-Hant', code: 'CH' }, 24 | { locale: 'ch-CHS', code: 'CHS' }, 25 | { locale: 'fi-FI', code: 'FI' }, 26 | { locale: 'sv-SE', code: 'Z' }, 27 | { locale: 'nl-NL', code: 'O' }, 28 | { locale: 'wes-PGW', code: 'PGW' }, 29 | { locale: 'sl-SI', code: 'SV' }, 30 | { locale: 'sw-KE', code: 'SW' }, 31 | { locale: 'et-EE', code: 'ST' }, 32 | { locale: 'ht-HT', code: 'CR' }, 33 | { locale: 'ilo-PH', code: 'IL' }, 34 | { locale: 'en-LR', code: 'ELI' }, 35 | { locale: 'rw-RW', code: 'YW' }, 36 | { locale: 'vi-VN', code: 'VT' }, 37 | { locale: 'ms-MY', code: 'ML' }, 38 | ]; 39 | -------------------------------------------------------------------------------- /src/locales/mg-MG/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Janoary", 3 | "februaryVariations": "Febroary", 4 | "marchVariations": "Martsa", 5 | "aprilVariations": "Aprily", 6 | "mayVariations": "Mey", 7 | "juneVariations": "Jona", 8 | "julyVariations": "Jolay", 9 | "augustVariations": "Aogositra", 10 | "septemberVariations": "Septambra", 11 | "octoberVariations": "Oktobra", 12 | "novemberVariations": "Novambra", 13 | "decemberVariations": "Desambra", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/mg-TND/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Janvie", 3 | "februaryVariations": "Fevrie", 4 | "marchVariations": "Marsa", 5 | "aprilVariations": "Avrily", 6 | "mayVariations": "Mey", 7 | "juneVariations": "Jona", 8 | "julyVariations": "Jolay", 9 | "augustVariations": "Aogositra", 10 | "septemberVariations": "Septambra", 11 | "octoberVariations": "Oktobra|Okotobra", 12 | "novemberVariations": "Novambra", 13 | "decemberVariations": "Desambra", 14 | "partMinutesSeparatorVariations": "min.|min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/mg-TNK/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Zanvie", 3 | "februaryVariations": "Fevrie", 4 | "marchVariations": "Marsy", 5 | "aprilVariations": "Avrily", 6 | "mayVariations": "May", 7 | "juneVariations": "Ziain", 8 | "julyVariations": "Zie|ZIE", 9 | "augustVariations": "Aoty", 10 | "septemberVariations": "Septambra", 11 | "octoberVariations": "Oktobra", 12 | "novemberVariations": "Novambra", 13 | "decemberVariations": "Desambra", 14 | "partMinutesSeparatorVariations": "min.|min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/mg-TTM/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Janoary", 3 | "februaryVariations": "Febroary", 4 | "marchVariations": "Martsa", 5 | "aprilVariations": "Aprily", 6 | "mayVariations": "Mey", 7 | "juneVariations": "Jona", 8 | "julyVariations": "Jolay", 9 | "augustVariations": "Aogositra", 10 | "septemberVariations": "Septambra", 11 | "octoberVariations": "Oktobra", 12 | "novemberVariations": "Novambra", 13 | "decemberVariations": "Desambra", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/mg-VZ/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Zanviè", 3 | "februaryVariations": "Fevriè|Feviè", 4 | "marchVariations": "Marsa", 5 | "aprilVariations": "Avrily", 6 | "mayVariations": "Mey", 7 | "juneVariations": "Ziein", 8 | "julyVariations": "Ziè", 9 | "augustVariations": "Oòh", 10 | "septemberVariations": "Septambra", 11 | "octoberVariations": "Oktobra", 12 | "novemberVariations": "Novambra", 13 | "decemberVariations": "Desambra", 14 | "partMinutesSeparatorVariations": "min.|min" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ms-MY/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Januari", 3 | "februaryVariations": "Februari", 4 | "marchVariations": "Mac", 5 | "aprilVariations": "April", 6 | "mayVariations": "Mei", 7 | "juneVariations": "Jun", 8 | "julyVariations": "Julai", 9 | "augustVariations": "Ogos", 10 | "septemberVariations": "September", 11 | "octoberVariations": "Oktober", 12 | "novemberVariations": "November", 13 | "decemberVariations": "Disember", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ne-NP/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/nl-NL/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "januari", 3 | "februaryVariations": "februari", 4 | "marchVariations": "maart", 5 | "aprilVariations": "april", 6 | "mayVariations": "mei", 7 | "juneVariations": "juni", 8 | "julyVariations": "juli", 9 | "augustVariations": "augustus", 10 | "septemberVariations": "september", 11 | "octoberVariations": "oktober", 12 | "novemberVariations": "november", 13 | "decemberVariations": "december", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/pl-PL/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Styczeń|stycznia", 3 | "februaryVariations": "Luty|lutego", 4 | "marchVariations": "Marzec|marca", 5 | "aprilVariations": "Kwiecień|kwietnia", 6 | "mayVariations": "maja|Maj", 7 | "juneVariations": "Czerwiec|czerwca", 8 | "julyVariations": "Lipiec|lipca", 9 | "augustVariations": "Sierpień|sierpnia", 10 | "septemberVariations": "Wrzesień|września", 11 | "octoberVariations": "października|Październik", 12 | "novemberVariations": "listopada|Listopad", 13 | "decemberVariations": "Grudzień|grudnia", 14 | "partMinutesSeparatorVariations": "min|min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/pt-POR/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "de janeiro|janeiro", 3 | "februaryVariations": "de fevereiro|fevereiro", 4 | "marchVariations": "de março|março", 5 | "aprilVariations": "de abril|abril", 6 | "mayVariations": "de maio|maio", 7 | "juneVariations": "de junho|junho", 8 | "julyVariations": "de julho|julho", 9 | "augustVariations": "de agosto|agosto", 10 | "septemberVariations": "de setembro|setembro", 11 | "octoberVariations": "de outubro|outubro", 12 | "novemberVariations": "de novembro|novembro", 13 | "decemberVariations": "de dezembro|dezembro", 14 | "partMinutesSeparatorVariations": "min|min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/pt-TPO/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "de janeiro|janeiro", 3 | "februaryVariations": "de fevereiro|fevereiro", 4 | "marchVariations": "de março|março", 5 | "aprilVariations": "de abril|abril", 6 | "mayVariations": "de maio|maio", 7 | "juneVariations": "de junho|junho", 8 | "julyVariations": "de julho|julho", 9 | "augustVariations": "de agosto|agosto", 10 | "septemberVariations": "de setembro|setembro", 11 | "octoberVariations": "de outubro|outubro", 12 | "novemberVariations": "de novembro|novembro", 13 | "decemberVariations": "de dezembro|dezembro", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ro-RO/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Ianuarie", 3 | "februaryVariations": "Februarie", 4 | "marchVariations": "Martie", 5 | "aprilVariations": "Aprilie", 6 | "mayVariations": "Mai", 7 | "juneVariations": "Iunie", 8 | "julyVariations": "Iulie", 9 | "augustVariations": "August", 10 | "septemberVariations": "Septembrie", 11 | "octoberVariations": "Octombrie", 12 | "novemberVariations": "Noiembrie", 13 | "decemberVariations": "Decembrie", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/ru-RU/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Январь|января", 3 | "februaryVariations": "Февраль|февраля", 4 | "marchVariations": "Март|марта", 5 | "aprilVariations": "Апрель|апреля", 6 | "mayVariations": "Май|мая", 7 | "juneVariations": "Июнь|июня", 8 | "julyVariations": "Июль|июля", 9 | "augustVariations": "Август|августа", 10 | "septemberVariations": "Сентябрь|сентября", 11 | "octoberVariations": "Октябрь|октября", 12 | "novemberVariations": "Ноябрь|ноября", 13 | "decemberVariations": "Декабрь|декабря", 14 | "partMinutesSeparatorVariations": "мин." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/rw-RW/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Mutarama", 3 | "februaryVariations": "Gashyantare", 4 | "marchVariations": "Werurwe", 5 | "aprilVariations": "Mata", 6 | "mayVariations": "Gicurasi", 7 | "juneVariations": "Kamena", 8 | "julyVariations": "Nyakanga", 9 | "augustVariations": "Kanama", 10 | "septemberVariations": "Nzeri", 11 | "octoberVariations": "Ukwakira", 12 | "novemberVariations": "Ugushyingo", 13 | "decemberVariations": "Ukuboza", 14 | "partMinutesSeparatorVariations": "Imin.|Umun." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/sl-SI/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "januar|januarja", 3 | "februaryVariations": "februar|februarja", 4 | "marchVariations": "marec|marca", 5 | "aprilVariations": "april|aprila", 6 | "mayVariations": "maj|maja", 7 | "juneVariations": "junij|junija", 8 | "julyVariations": "julij|julija", 9 | "augustVariations": "avgust|avgusta", 10 | "septemberVariations": "september|septembra", 11 | "octoberVariations": "oktober|oktobra", 12 | "novemberVariations": "november|novembra", 13 | "decemberVariations": "december|decembra", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/sv-SE/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "januari", 3 | "februaryVariations": "februari", 4 | "marchVariations": "mars", 5 | "aprilVariations": "april", 6 | "mayVariations": "maj", 7 | "juneVariations": "juni", 8 | "julyVariations": "juli", 9 | "augustVariations": "augusti", 10 | "septemberVariations": "september", 11 | "octoberVariations": "oktober", 12 | "novemberVariations": "november", 13 | "decemberVariations": "december", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/sw-KE/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Januari", 3 | "februaryVariations": "Februari", 4 | "marchVariations": "Machi", 5 | "aprilVariations": "Aprili", 6 | "mayVariations": "Mei", 7 | "juneVariations": "Juni", 8 | "julyVariations": "Julai", 9 | "augustVariations": "Agosti", 10 | "septemberVariations": "Septemba", 11 | "octoberVariations": "Oktoba", 12 | "novemberVariations": "Novemba", 13 | "decemberVariations": "Desemba", 14 | "partMinutesSeparatorVariations": "Dak." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/tl-PH/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Enero", 3 | "februaryVariations": "Pebrero", 4 | "marchVariations": "Marso", 5 | "aprilVariations": "Abril", 6 | "mayVariations": "Mayo", 7 | "juneVariations": "Hunyo", 8 | "julyVariations": "Hulyo", 9 | "augustVariations": "Agosto", 10 | "septemberVariations": "Setyembre", 11 | "octoberVariations": "Oktubre", 12 | "novemberVariations": "Nobyembre", 13 | "decemberVariations": "Disyembre", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/tr-TR/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Ocak", 3 | "februaryVariations": "Şubat", 4 | "marchVariations": "Mart", 5 | "aprilVariations": "Nisan|NİSAN", 6 | "mayVariations": "MAYIS|Mayıs", 7 | "juneVariations": "HAZİRAN|Haziran", 8 | "julyVariations": "Temmuz", 9 | "augustVariations": "Ağustos", 10 | "septemberVariations": "Eylül", 11 | "octoberVariations": "Ekim|EKİM", 12 | "novemberVariations": "Kasım|KASIM", 13 | "decemberVariations": "Aralık|ARALIK", 14 | "partMinutesSeparatorVariations": "dk.|dk" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/tw-TW/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "Simma" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/uk-UA/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "Січень|січень|січня", 3 | "februaryVariations": "Лютий|лютий|лютого", 4 | "marchVariations": "Березень|березень|березня", 5 | "aprilVariations": "Квітень|квітень|квітня", 6 | "mayVariations": "Травень|травень|травня", 7 | "juneVariations": "Червень|червень|червня", 8 | "julyVariations": "Липень|липень|липня", 9 | "augustVariations": "Серпень|серпень|серпня", 10 | "septemberVariations": "Вересень|вересень|вересня", 11 | "octoberVariations": "Жовтень|жовтень|жовтня", 12 | "novemberVariations": "Листопад|листопад|листопада", 13 | "decemberVariations": "Грудень|грудень|грудня", 14 | "partMinutesSeparatorVariations": "хв" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/vi-VN/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "tháng 1", 3 | "februaryVariations": "tháng 2", 4 | "marchVariations": "tháng 3", 5 | "aprilVariations": "tháng 4", 6 | "mayVariations": "tháng 5", 7 | "juneVariations": "tháng 6", 8 | "julyVariations": "tháng 7", 9 | "augustVariations": "tháng 8", 10 | "septemberVariations": "tháng 9", 11 | "octoberVariations": "tháng 10", 12 | "novemberVariations": "tháng 11", 13 | "decemberVariations": "tháng 12", 14 | "partMinutesSeparatorVariations": "phút" 15 | } 16 | -------------------------------------------------------------------------------- /src/locales/wes-PGW/text.json: -------------------------------------------------------------------------------- 1 | { 2 | "januaryVariations": "January", 3 | "februaryVariations": "February", 4 | "marchVariations": "March", 5 | "aprilVariations": "April", 6 | "mayVariations": "May", 7 | "juneVariations": "June", 8 | "julyVariations": "July", 9 | "augustVariations": "August", 10 | "septemberVariations": "September", 11 | "octoberVariations": "October", 12 | "novemberVariations": "November", 13 | "decemberVariations": "December", 14 | "partMinutesSeparatorVariations": "min." 15 | } 16 | -------------------------------------------------------------------------------- /src/node/index.ts: -------------------------------------------------------------------------------- 1 | import './utils.node.js'; 2 | import { startParse } from '../common/parser.js'; 3 | import { validateInput } from '../common/epub_validation.js'; 4 | import { MWBSchedule, WSchedule } from '../types/index.js'; 5 | 6 | export type { MWBSchedule, WSchedule } 7 | 8 | export const loadEPUB = async (epubInput: string | Blob | { url: string }) => { 9 | try { 10 | validateInput(epubInput); 11 | 12 | const data = await startParse(epubInput); 13 | return data as any; 14 | } catch (err) { 15 | console.error(err); 16 | 17 | throw new Error((err as Error)?.message); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/node/utils.node.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { readFile } from 'fs/promises'; 3 | 4 | import CH from '../locales/cmn-Hant/text.json' with { type: 'json' }; 5 | import CHS from '../locales/ch-CHS/text.json' with { type: 'json' }; 6 | import CR from '../locales/ht-HT/text.json' with { type: 'json' }; 7 | import E from '../locales/en/text.json' with { type: 'json' }; 8 | import ELI from '../locales/en-LR/text.json' with { type: 'json' }; 9 | import F from '../locales/fr-FR/text.json' with { type: 'json' }; 10 | import FI from '../locales/fi-FI/text.json' with { type: 'json' }; 11 | import I from '../locales/it-IT/text.json' with { type: 'json' }; 12 | import IL from '../locales/ilo-PH/text.json' with { type: 'json' }; 13 | import J from '../locales/ja-JP/text.json' with { type: 'json' }; 14 | import K from '../locales/uk-UA/text.json' with { type: 'json' }; 15 | import KO from '../locales/ko-KR/text.json' with { type: 'json' }; 16 | import M from '../locales/ro-RO/text.json' with { type: 'json' }; 17 | import MG from '../locales/mg-MG/text.json' with { type: 'json' }; 18 | import ML from '../locales/ms-MY/text.json' with { type: 'json' }; 19 | import O from '../locales/nl-NL/text.json' with { type: 'json' }; 20 | import P from '../locales/pl-PL/text.json' with { type: 'json' }; 21 | import PGW from '../locales/wes-PGW/text.json' with { type: 'json' }; 22 | import S from '../locales/es-ES/text.json' with { type: 'json' }; 23 | import ST from '../locales/et-EE/text.json' with { type: 'json' }; 24 | import SV from '../locales/sl-SI/text.json' with { type: 'json' }; 25 | import SW from '../locales/sw-KE/text.json' with { type: 'json' }; 26 | import T from '../locales/pt-POR/text.json' with { type: 'json' }; 27 | import TG from '../locales/tl-PH/text.json' with { type: 'json' }; 28 | import TK from '../locales/tr-TR/text.json' with { type: 'json' }; 29 | import TND from '../locales/mg-TND/text.json' with { type: 'json' }; 30 | import TNK from '../locales/mg-TNK/text.json' with { type: 'json' }; 31 | import TPO from '../locales/pt-TPO/text.json' with { type: 'json' }; 32 | import TTM from '../locales/mg-TTM/text.json' with { type: 'json' }; 33 | import TW from '../locales/tw-TW/text.json' with { type: 'json' }; 34 | import U from '../locales/ru-RU/text.json' with { type: 'json' }; 35 | import VT from '../locales/vi-VN/text.json' with { type: 'json' }; 36 | import VZ from '../locales/mg-VZ/text.json' with { type: 'json' }; 37 | import X from '../locales/de-DE/text.json' with { type: 'json' }; 38 | import YW from '../locales/rw-RW/text.json' with { type: 'json' }; 39 | import Z from '../locales/sv-SE/text.json' with { type: 'json' }; 40 | 41 | declare global { 42 | var jw_epub_parser: any; 43 | } 44 | 45 | global.jw_epub_parser = { 46 | languages: { 47 | CH, 48 | CHS, 49 | CR, 50 | E, 51 | ELI, 52 | F, 53 | FI, 54 | I, 55 | IL, 56 | J, 57 | K, 58 | KO, 59 | M, 60 | MG, 61 | ML, 62 | O, 63 | P, 64 | PGW, 65 | S, 66 | ST, 67 | SV, 68 | SW, 69 | T, 70 | TG, 71 | TK, 72 | TND, 73 | TNK, 74 | TPO, 75 | TTM, 76 | TW, 77 | U, 78 | VT, 79 | VZ, 80 | X, 81 | YW, 82 | Z, 83 | }, 84 | path: path, 85 | readFile: readFile, 86 | }; 87 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type MWBSchedule = { 2 | mwb_week_date: string; 3 | mwb_week_date_locale?: string; 4 | mwb_weekly_bible_reading: string; 5 | mwb_song_first: number; 6 | mwb_tgw_talk: string; 7 | mwb_tgw_talk_title?: string; 8 | mwb_tgw_gems_title?: string; 9 | mwb_tgw_bread: string; 10 | mwb_tgw_bread_title?: string; 11 | mwb_ayf_count: number; 12 | mwb_ayf_part1: string; 13 | mwb_ayf_part1_time?: number; 14 | mwb_ayf_part1_type?: string; 15 | mwb_ayf_part1_title?: string; 16 | mwb_ayf_part2: string; 17 | mwb_ayf_part2_time?: number; 18 | mwb_ayf_part2_type?: string; 19 | mwb_ayf_part2_title?: string; 20 | mwb_ayf_part3: string; 21 | mwb_ayf_part3_time?: number; 22 | mwb_ayf_part3_type?: string; 23 | mwb_ayf_part3_title?: string; 24 | mwb_ayf_part4?: string; 25 | mwb_ayf_part4_time?: number; 26 | mwb_ayf_part4_type?: string; 27 | mwb_ayf_part4_title?: string; 28 | mwb_song_middle: number | string; 29 | mwb_lc_count: number; 30 | mwb_lc_part1: string; 31 | mwb_lc_part1_time?: number; 32 | mwb_lc_part1_content?: string; 33 | mwb_lc_part1_title?: string; 34 | mwb_lc_part2: string; 35 | mwb_lc_part2_time?: number; 36 | mwb_lc_part2_content?: string; 37 | mwb_lc_part2_title?: string; 38 | mwb_lc_cbs: string; 39 | mwb_lc_cbs_title?: string; 40 | mwb_song_conclude: number | string; 41 | }; 42 | 43 | export type WSchedule = { 44 | w_study_date: string; 45 | w_study_date_locale?: string; 46 | w_study_title: string; 47 | w_study_opening_song?: number; 48 | w_study_concluding_song?: number; 49 | }; 50 | 51 | export interface Language { 52 | [key: string]: { 53 | januaryVariations: string; 54 | februaryVariations: string; 55 | marchVariations: string; 56 | aprilVariations: string; 57 | mayVariations: string; 58 | juneVariations: string; 59 | julyVariations: string; 60 | augustVariations: string; 61 | septemberVariations: string; 62 | octoberVariations: string; 63 | novemberVariations: string; 64 | decemberVariations: string; 65 | partMinutesSeparatorVariations: string; 66 | }; 67 | } 68 | 69 | export type LangRegExp = { 70 | [lang: string]: RegExp; 71 | }; 72 | 73 | export type WDateParsingResult = [year: string, month: string, date: string]; 74 | 75 | export type WDateParsing = { 76 | [lang: string]: (groups: string[]) => WDateParsingResult; 77 | }; 78 | 79 | export type MWBDateParsingResult = [month: string, date: string]; 80 | 81 | export type MWBDateParsing = { 82 | [lang: string]: (groups: string[]) => MWBDateParsingResult; 83 | }; 84 | 85 | export type Override = { 86 | [language: string]: { 87 | [src: string]: string; 88 | }; 89 | }; 90 | -------------------------------------------------------------------------------- /test/01_standardParsing.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as path from 'path'; 3 | import { expect } from 'chai'; 4 | import { loadEPUB } from '../dist/node/index.js'; 5 | 6 | const list = JSON.parse(await fs.promises.readFile(new URL('./standardParsing/list.json', import.meta.url))); 7 | const JW_CDN = 'https://b.jw-cdn.org/apis/pub-media/GETPUBMEDIALINKS?'; 8 | 9 | const fetchData = async (language, issue, pub) => { 10 | let data = []; 11 | let fixture = []; 12 | 13 | const url = 14 | JW_CDN + 15 | new URLSearchParams({ 16 | langwritten: language, 17 | pub, 18 | fileformat: 'epub', 19 | output: 'json', 20 | issue, 21 | }); 22 | 23 | const res = await fetch(url); 24 | 25 | if (res.status === 200) { 26 | const result = await res.json(); 27 | const epubEntry = result.files[language].EPUB; 28 | 29 | const epubFile = epubEntry[0].file; 30 | const epubUrl = epubFile.url; 31 | const epubName = path.basename(epubUrl); 32 | data = await loadEPUB({ url: epubUrl }); 33 | fixture = (await import(`./fixtures/${epubName.replace('.epub', '.js')}`)).default; 34 | } 35 | 36 | return { data, fixture }; 37 | }; 38 | 39 | describe('Testing Standard Parsing', () => { 40 | for (let i = 0; i < list.length; i++) { 41 | const { language, issue } = list[i]; 42 | 43 | describe(`Test loadEPUB function for ${language} language`, async () => { 44 | it(`Parsing Meeting Workbook EPUB file`, async () => { 45 | const { data, fixture } = await fetchData(language, issue, 'mwb'); 46 | 47 | for (let a = 0; a < fixture.length; a++) { 48 | const week = fixture[a]; 49 | for (let [key, value] of Object.entries(week)) { 50 | expect(data[a]).to.have.property(key).equal(value); 51 | } 52 | } 53 | }); 54 | 55 | it(`Parsing Watchtower Study EPUB file`, async () => { 56 | const { data, fixture } = await fetchData(language, issue, 'w'); 57 | 58 | for (let a = 0; a < fixture.length; a++) { 59 | const week = fixture[a]; 60 | for (let [key, value] of Object.entries(week)) { 61 | expect(data[a]).to.have.property(key).equal(value); 62 | } 63 | } 64 | }); 65 | }); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /test/02_enhancedParsing.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as path from 'path'; 3 | import { expect } from 'chai'; 4 | import { loadEPUB } from '../dist/node/index.js'; 5 | 6 | const list = JSON.parse(await fs.promises.readFile(new URL('./enhancedParsing/list.json', import.meta.url))); 7 | 8 | const JW_CDN = 'https://b.jw-cdn.org/apis/pub-media/GETPUBMEDIALINKS?'; 9 | 10 | const fetchIssueData = async (issue, pub) => { 11 | try { 12 | if (issue.hasEPUB) { 13 | const epubFile = issue.hasEPUB[0].file; 14 | const epubUrl = epubFile.url; 15 | 16 | const epubData = await loadEPUB({ url: epubUrl }); 17 | return epubData; 18 | } 19 | 20 | if (!issue.hasEPUB) { 21 | return []; 22 | } 23 | } catch (err) { 24 | throw new Error(err); 25 | } 26 | }; 27 | 28 | const fetchData = async (language, issue, pub) => { 29 | let data = []; 30 | let fixture = []; 31 | 32 | const url = 33 | JW_CDN + 34 | new URLSearchParams({ 35 | langwritten: language, 36 | pub, 37 | output: 'json', 38 | issue, 39 | }); 40 | 41 | const res = await fetch(url); 42 | 43 | if (res.status === 200) { 44 | const result = await res.json(); 45 | const hasEPUB = result.files[language].EPUB; 46 | 47 | const issueFetch = { issueDate: issue, currentYear: issue.substring(0, 4), language, hasEPUB: hasEPUB }; 48 | 49 | data = await fetchIssueData(issueFetch, pub); 50 | 51 | if (hasEPUB) { 52 | const epubFile = hasEPUB[0].file; 53 | const epubUrl = epubFile.url; 54 | const epubName = path.basename(epubUrl); 55 | fixture = (await import(`./fixtures/${epubName.replace('.epub', '.js')}`)).default; 56 | } 57 | 58 | if (!hasEPUB) { 59 | fixture = (await import(`./fixtures/${pub}_${language}_${issue}.js`)).default; 60 | } 61 | } 62 | 63 | return { data, fixture }; 64 | }; 65 | 66 | describe('Testing Enhanced Parsing', async () => { 67 | for (let i = 0; i < list.length; i++) { 68 | const { language, issue } = list[i]; 69 | 70 | describe(`Test loadEPUB function for ${language} language`, async () => { 71 | it(`Parsing Meeting Workbook EPUB file`, async () => { 72 | const { data, fixture } = await fetchData(language, issue, 'mwb'); 73 | 74 | for (let a = 0; a < fixture.length; a++) { 75 | const week = fixture[a]; 76 | for (let [key, value] of Object.entries(week)) { 77 | expect(data[a]).to.have.property(key).equal(value); 78 | } 79 | } 80 | }); 81 | 82 | it(`Parsing Watchtower Study EPUB file`, async () => { 83 | const { data, fixture } = await fetchData(language, issue, 'w'); 84 | 85 | for (let a = 0; a < fixture.length; a++) { 86 | const week = fixture[a]; 87 | for (let [key, value] of Object.entries(week)) { 88 | expect(data[a]).to.have.property(key).equal(value); 89 | } 90 | } 91 | }); 92 | }); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /test/enhancedParsing/list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "language": "E", "issue": "202411" }, 3 | { "language": "CH", "issue": "202411" }, 4 | { "language": "CHS", "issue": "202411" }, 5 | { "language": "F", "issue": "202411" }, 6 | { "language": "I", "issue": "202411" }, 7 | { "language": "J", "issue": "202411" }, 8 | { "language": "K", "issue": "202411" }, 9 | { "language": "KO", "issue": "202411" }, 10 | { "language": "M", "issue": "202411" }, 11 | { "language": "MG", "issue": "202411" }, 12 | { "language": "S", "issue": "202411" }, 13 | { "language": "TG", "issue": "202411" }, 14 | { "language": "TK", "issue": "202411" }, 15 | { "language": "TND", "issue": "202411" }, 16 | { "language": "TNK", "issue": "202411" }, 17 | { "language": "TPO", "issue": "202411" }, 18 | { "language": "U", "issue": "202411" }, 19 | { "language": "VZ", "issue": "202411" }, 20 | { "language": "X", "issue": "202411" }, 21 | { "language": "O", "issue": "202411" }, 22 | { "language": "Z", "issue": "202411" }, 23 | { "language": "PGW", "issue": "202411" }, 24 | { "language": "SV", "issue": "202411" }, 25 | { "language": "SW", "issue": "202411" }, 26 | { "language": "CR", "issue": "202411" }, 27 | { "language": "IL", "issue": "202411" }, 28 | { "language": "ELI", "issue": "202407" }, 29 | { "language": "YW", "issue": "202501" }, 30 | { "language": "VT", "issue": "202501" }, 31 | { "language": "ML", "issue": "202501" } 32 | ] 33 | -------------------------------------------------------------------------------- /test/fixtures/mwb_CHS_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | mwb_week_date: '2024/11/04', 4 | mwb_week_date_locale: '11月4-10日', 5 | mwb_weekly_bible_reading: '诗篇105篇', 6 | mwb_song_first: 3, 7 | mwb_tgw_talk: '“他记得他的誓约,直到万世”', 8 | mwb_tgw_talk_title: '1.“他记得他的誓约,直到万世”', 9 | mwb_tgw_gems_title: '2.经文宝石', 10 | mwb_tgw_bread: '诗105:24-45(《教导》第5课)', 11 | mwb_tgw_bread_title: '3.经文朗读', 12 | mwb_ayf_count: 4, 13 | mwb_ayf_part1: '挨家挨户。对方很忙。(《爱心》第2课第5点)', 14 | mwb_ayf_part1_time: 1, 15 | mwb_ayf_part1_type: '开始交谈', 16 | mwb_ayf_part1_title: '4.开始交谈', 17 | mwb_ayf_part2: '挨家挨户。对方想要争论,你客气地结束谈话。(《爱心》第4课第5点)', 18 | mwb_ayf_part2_time: 2, 19 | mwb_ayf_part2_type: '开始交谈', 20 | mwb_ayf_part2_title: '5.开始交谈', 21 | mwb_ayf_part3: '挨家挨户。上次对方对某个话题感兴趣,这次你给他看一本相关的杂志。(《爱心》第8课第3点)', 22 | mwb_ayf_part3_time: 4, 23 | mwb_ayf_part3_type: '继续帮助对方', 24 | mwb_ayf_part3_title: '6.继续帮助对方', 25 | mwb_ayf_part4: '非正式见证。向对方介绍JW Library® app,帮助他下载。(《爱心》第9课第5点)', 26 | mwb_ayf_part4_time: 4, 27 | mwb_ayf_part4_type: '继续帮助对方', 28 | mwb_ayf_part4_title: '7.继续帮助对方', 29 | mwb_song_middle: 84, 30 | mwb_lc_count: 1, 31 | mwb_lc_part1: '献出你的爱', 32 | mwb_lc_part1_time: 15, 33 | mwb_lc_part1_title: '8.献出你的爱', 34 | mwb_lc_part1_content: '节目包括讨论。', 35 | mwb_lc_cbs: '《作见证》第17章13-19段', 36 | mwb_lc_cbs_title: '9.会众研经班', 37 | mwb_song_conclude: 97, 38 | }, 39 | { 40 | mwb_week_date: '2024/11/11', 41 | mwb_week_date_locale: '11月11-17日', 42 | mwb_weekly_bible_reading: '诗篇106篇', 43 | mwb_song_first: 36, 44 | mwb_tgw_talk: '“他们忘记了上帝他们的救主”', 45 | mwb_tgw_talk_title: '1.“他们忘记了上帝他们的救主”', 46 | mwb_tgw_gems_title: '2.经文宝石', 47 | mwb_tgw_bread: '诗106:21-48(《教导》第10课)', 48 | mwb_tgw_bread_title: '3.经文朗读', 49 | mwb_ayf_count: 2, 50 | mwb_ayf_part1: '节目包括讨论。先观看短片,然后讨论《爱心》第11课1-2点。', 51 | mwb_ayf_part1_time: 7, 52 | mwb_ayf_part1_type: '简单明了——耶稣怎么做', 53 | mwb_ayf_part1_title: '4.简单明了——耶稣怎么做', 54 | mwb_ayf_part2: '讨论《爱心》第11课3-5点,以及“请看”的经文。', 55 | mwb_ayf_part2_time: 8, 56 | mwb_ayf_part2_type: '简单明了——向耶稣学习', 57 | mwb_ayf_part2_title: '5.简单明了——向耶稣学习', 58 | mwb_song_middle: 78, 59 | mwb_lc_count: 1, 60 | mwb_lc_part1: '本地需要', 61 | mwb_lc_part1_time: 15, 62 | mwb_lc_part1_title: '6.本地需要', 63 | mwb_lc_cbs: '《作见证》第18章1-5段,以及142和144页的附栏', 64 | mwb_lc_cbs_title: '7.会众研经班', 65 | mwb_song_conclude: 77, 66 | }, 67 | { 68 | mwb_week_date: '2024/11/18', 69 | mwb_week_date_locale: '11月18-24日', 70 | mwb_weekly_bible_reading: '诗篇107-108篇', 71 | mwb_song_first: 7, 72 | mwb_tgw_talk: '“要感谢耶和华,因为他确实良善”', 73 | mwb_tgw_talk_title: '1.“要感谢耶和华,因为他确实良善”', 74 | mwb_tgw_gems_title: '2.经文宝石', 75 | mwb_tgw_bread: '诗107:1-28(《教导》第5课)', 76 | mwb_tgw_bread_title: '3.经文朗读', 77 | mwb_ayf_count: 3, 78 | mwb_ayf_part1: '非正式见证。(《爱心》第1课第4点)', 79 | mwb_ayf_part1_time: 3, 80 | mwb_ayf_part1_type: '开始交谈', 81 | mwb_ayf_part1_title: '4.开始交谈', 82 | mwb_ayf_part2: '非正式见证。介绍圣经课程,给对方一张报名学习圣经的名片。(《爱心》第9课第3点)', 83 | mwb_ayf_part2_time: 4, 84 | mwb_ayf_part2_type: '继续帮助对方', 85 | mwb_ayf_part2_title: '5.继续帮助对方', 86 | mwb_ayf_part3: '《青年人》第90篇——主题:我的想法总是很消极,怎么办?(《教导》第14课)', 87 | mwb_ayf_part3_time: 5, 88 | mwb_ayf_part3_type: '演讲', 89 | mwb_ayf_part3_title: '6.演讲', 90 | mwb_song_middle: 46, 91 | mwb_lc_count: 1, 92 | mwb_lc_part1: '用歌声感谢耶和华', 93 | mwb_lc_part1_time: 15, 94 | mwb_lc_part1_title: '7.用歌声感谢耶和华', 95 | mwb_lc_part1_content: '节目包括讨论。', 96 | mwb_lc_cbs: '《作见证》第18章6-15段', 97 | mwb_lc_cbs_title: '8.会众研经班', 98 | mwb_song_conclude: 73, 99 | }, 100 | { 101 | mwb_week_date: '2024/11/25', 102 | mwb_week_date_locale: '11月25日~12月1日', 103 | mwb_weekly_bible_reading: '诗篇109-112篇', 104 | mwb_song_first: 14, 105 | mwb_tgw_talk: '拥护我们的君王耶稣!', 106 | mwb_tgw_talk_title: '1.拥护我们的君王耶稣!', 107 | mwb_tgw_gems_title: '2.经文宝石', 108 | mwb_tgw_bread: '诗109:1-26(《教导》第2课)', 109 | mwb_tgw_bread_title: '3.经文朗读', 110 | mwb_ayf_count: 3, 111 | mwb_ayf_part1: '挨家挨户。用一张传单的内容开始交谈。(《爱心》第4课第3点)', 112 | mwb_ayf_part1_time: 2, 113 | mwb_ayf_part1_type: '开始交谈', 114 | mwb_ayf_part1_title: '4.开始交谈', 115 | mwb_ayf_part2: '示范。《常见问题》第23篇——主题:为什么耶和华见证人不参战?(《爱心》第4课第4点)', 116 | mwb_ayf_part2_time: 5, 117 | mwb_ayf_part2_type: '解释自己的信仰', 118 | mwb_ayf_part2_title: '5.解释自己的信仰', 119 | mwb_ayf_part3: '《美好生命》第15课第6点,以及“如果有人说”(《爱心》第11课第3点)', 120 | mwb_ayf_part3_time: 5, 121 | mwb_ayf_part3_type: '教导人成为门徒', 122 | mwb_ayf_part3_title: '6.教导人成为门徒', 123 | mwb_song_middle: 72, 124 | mwb_lc_count: 1, 125 | mwb_lc_part1: '支持王国就该怎么做?', 126 | mwb_lc_part1_time: 15, 127 | mwb_lc_part1_title: '7.支持王国就该怎么做?', 128 | mwb_lc_part1_content: '节目包括讨论。', 129 | mwb_lc_cbs: '《作见证》第18章16-24段', 130 | mwb_lc_cbs_title: '8.会众研经班', 131 | mwb_song_conclude: 75, 132 | }, 133 | { 134 | mwb_week_date: '2024/12/02', 135 | mwb_week_date_locale: '12月2-8日', 136 | mwb_weekly_bible_reading: '诗篇113-118篇', 137 | mwb_song_first: 127, 138 | mwb_tgw_talk: '拿什么报答耶和华?', 139 | mwb_tgw_talk_title: '1.拿什么报答耶和华?', 140 | mwb_tgw_gems_title: '2.经文宝石', 141 | mwb_tgw_bread: '诗116:1-117:2(《教导》第2课)', 142 | mwb_tgw_bread_title: '3.经文朗读', 143 | mwb_ayf_count: 2, 144 | mwb_ayf_part1: '节目包括讨论。先观看短片,然后讨论《爱心》第12课1-2点。', 145 | mwb_ayf_part1_time: 7, 146 | mwb_ayf_part1_type: '坦率——耶稣怎么做', 147 | mwb_ayf_part1_title: '4.坦率——耶稣怎么做', 148 | mwb_ayf_part2: '讨论《爱心》第12课3-5点,以及“请看”的经文。', 149 | mwb_ayf_part2_time: 8, 150 | mwb_ayf_part2_type: '坦率——向耶稣学习', 151 | mwb_ayf_part2_title: '5.坦率——向耶稣学习', 152 | mwb_song_middle: 60, 153 | mwb_lc_count: 1, 154 | mwb_lc_part1: '本地需要', 155 | mwb_lc_part1_time: 15, 156 | mwb_lc_part1_title: '6.本地需要', 157 | mwb_lc_cbs: '《作见证》第19章1-5段,以及149-150页的附栏', 158 | mwb_lc_cbs_title: '7.会众研经班', 159 | mwb_song_conclude: 29, 160 | }, 161 | { 162 | mwb_week_date: '2024/12/09', 163 | mwb_week_date_locale: '12月9-15日', 164 | mwb_weekly_bible_reading: '诗篇119:1-56', 165 | mwb_song_first: 124, 166 | mwb_tgw_talk: '“年轻人怎样才能有纯洁的行为呢?”', 167 | mwb_tgw_talk_title: '1.“年轻人怎样才能有纯洁的行为呢?”', 168 | mwb_tgw_gems_title: '2.经文宝石', 169 | mwb_tgw_bread: '诗119:1-32(《教导》第5课)', 170 | mwb_tgw_bread_title: '3.经文朗读', 171 | mwb_ayf_count: 3, 172 | mwb_ayf_part1: '非正式见证。挨家挨户传道时,跟街上遇到的行人聊天。(《爱心》第1课第4点)', 173 | mwb_ayf_part1_time: 3, 174 | mwb_ayf_part1_type: '开始交谈', 175 | mwb_ayf_part1_title: '4.开始交谈', 176 | mwb_ayf_part2: '挨家挨户。上次对方说,他的家人最近去世了。(《爱心》第9课第3点)', 177 | mwb_ayf_part2_time: 4, 178 | mwb_ayf_part2_type: '继续帮助对方', 179 | mwb_ayf_part2_title: '5.继续帮助对方', 180 | mwb_ayf_part3: '《青年人》第83篇——主题:我怎样才能抗拒诱惑?(《教导》第20课)', 181 | mwb_ayf_part3_time: 5, 182 | mwb_ayf_part3_type: '演讲', 183 | mwb_ayf_part3_title: '6.演讲', 184 | mwb_song_middle: 40, 185 | mwb_lc_count: 2, 186 | mwb_lc_part1: '12 月的《组织成就了什么事》', 187 | mwb_lc_part1_time: 10, 188 | mwb_lc_part1_title: '7.12 月的《组织成就了什么事》', 189 | mwb_lc_part1_content: '播放短片。', 190 | mwb_lc_part2: '本地需要', 191 | mwb_lc_part2_time: 5, 192 | mwb_lc_part2_title: '8.本地需要', 193 | mwb_lc_cbs: '《作见证》第19章6-13段', 194 | mwb_lc_cbs_title: '9.会众研经班', 195 | mwb_song_conclude: 21, 196 | }, 197 | { 198 | mwb_week_date: '2024/12/16', 199 | mwb_week_date_locale: '12月16-22日', 200 | mwb_weekly_bible_reading: '诗篇119:57-120', 201 | mwb_song_first: 129, 202 | mwb_tgw_talk: '怎样忍受考验', 203 | mwb_tgw_talk_title: '1.怎样忍受考验', 204 | mwb_tgw_gems_title: '2.经文宝石', 205 | mwb_tgw_bread: '诗119:57-80(《教导》第12课)', 206 | mwb_tgw_bread_title: '3.经文朗读', 207 | mwb_ayf_count: 3, 208 | mwb_ayf_part1: '挨家挨户。给对方看我们的网站,并给他一张JW网站名片。(《爱心》第2课第5点)', 209 | mwb_ayf_part1_time: 3, 210 | mwb_ayf_part1_type: '开始交谈', 211 | mwb_ayf_part1_title: '4.开始交谈', 212 | mwb_ayf_part2: 213 | '非正式见证。邀请对方来听下周的公众演讲,给他看短片《王国聚会所的聚会是怎样的?》。(《爱心》第8课第3点)', 214 | mwb_ayf_part2_time: 4, 215 | mwb_ayf_part2_type: '继续帮助对方', 216 | mwb_ayf_part2_title: '5.继续帮助对方', 217 | mwb_ayf_part3: '示范。《圣经问答》第157篇——主题:关于自然灾害,圣经怎么说?(《爱心》第3课第3点)', 218 | mwb_ayf_part3_time: 5, 219 | mwb_ayf_part3_type: '解释自己的信仰', 220 | mwb_ayf_part3_title: '6.解释自己的信仰', 221 | mwb_song_middle: 128, 222 | mwb_lc_count: 1, 223 | mwb_lc_part1: '耶和华会帮助我们忍耐下去', 224 | mwb_lc_part1_time: 15, 225 | mwb_lc_part1_title: '7.耶和华会帮助我们忍耐下去', 226 | mwb_lc_part1_content: '节目包括讨论。', 227 | mwb_lc_cbs: '《作见证》第19章14-20段,以及152页的附栏', 228 | mwb_lc_cbs_title: '8.会众研经班', 229 | mwb_song_conclude: 32, 230 | }, 231 | { 232 | mwb_week_date: '2024/12/23', 233 | mwb_week_date_locale: '12月23-29日', 234 | mwb_weekly_bible_reading: '诗篇119:121-176', 235 | mwb_song_first: 31, 236 | mwb_tgw_talk: '有些痛苦是可以避免的', 237 | mwb_tgw_talk_title: '1.有些痛苦是可以避免的', 238 | mwb_tgw_gems_title: '2.经文宝石', 239 | mwb_tgw_bread: '诗119:121-152(《教导》第2课)', 240 | mwb_tgw_bread_title: '3.经文朗读', 241 | mwb_ayf_count: 3, 242 | mwb_ayf_part1: '挨家挨户。(《爱心》第1课第5点)', 243 | mwb_ayf_part1_time: 3, 244 | mwb_ayf_part1_type: '开始交谈', 245 | mwb_ayf_part1_title: '4.开始交谈', 246 | mwb_ayf_part2: '挨家挨户。告诉对方怎样在jw.org上找到他感兴趣的内容。(《爱心》第8课第3点)', 247 | mwb_ayf_part2_time: 4, 248 | mwb_ayf_part2_type: '继续帮助对方', 249 | mwb_ayf_part2_title: '5.继续帮助对方', 250 | mwb_ayf_part3: '学生不经常参加聚会,跟他谈谈这个问题。(《爱心》第12课第4点)', 251 | mwb_ayf_part3_time: 5, 252 | mwb_ayf_part3_type: '教导人成为门徒', 253 | mwb_ayf_part3_title: '6.教导人成为门徒', 254 | mwb_song_middle: 121, 255 | mwb_lc_count: 1, 256 | mwb_lc_part1: '不要因财“受伤”', 257 | mwb_lc_part1_time: 15, 258 | mwb_lc_part1_title: '7.不要因财“受伤”', 259 | mwb_lc_part1_content: '节目包括讨论。', 260 | mwb_lc_cbs: '《作见证》第7部分引言,以及第20章1-7段', 261 | mwb_lc_cbs_title: '8.会众研经班', 262 | mwb_song_conclude: 101, 263 | }, 264 | { 265 | mwb_week_date: '2024/12/30', 266 | mwb_week_date_locale: '2024年12月30日~2025年1月5日', 267 | mwb_weekly_bible_reading: '诗篇120-126篇', 268 | mwb_song_first: 144, 269 | mwb_tgw_talk: '他们流泪撒种,欢呼收割', 270 | mwb_tgw_talk_title: '1.他们流泪撒种,欢呼收割', 271 | mwb_tgw_gems_title: '2.经文宝石', 272 | mwb_tgw_bread: '诗124:1-126:6(《教导》第5课)', 273 | mwb_tgw_bread_title: '3.经文朗读', 274 | mwb_ayf_count: 3, 275 | mwb_ayf_part1: '公众场所见证。(《爱心》第3课第5点)', 276 | mwb_ayf_part1_time: 3, 277 | mwb_ayf_part1_type: '开始交谈', 278 | mwb_ayf_part1_title: '4.开始交谈', 279 | mwb_ayf_part2: '挨家挨户。上次对方说圣经不切实际。(《爱心》第9课第5点)', 280 | mwb_ayf_part2_time: 4, 281 | mwb_ayf_part2_type: '继续帮助对方', 282 | mwb_ayf_part2_title: '5.继续帮助对方', 283 | mwb_ayf_part3: '《美好生命》第16课引言,以及1-3点(《爱心》第11课第3点)', 284 | mwb_ayf_part3_time: 5, 285 | mwb_ayf_part3_type: '教导人成为门徒', 286 | mwb_ayf_part3_title: '6.教导人成为门徒', 287 | mwb_song_middle: 155, 288 | mwb_lc_count: 1, 289 | mwb_lc_part1: '因上帝的承诺而快乐', 290 | mwb_lc_part1_time: 15, 291 | mwb_lc_part1_title: '7.因上帝的承诺而快乐', 292 | mwb_lc_part1_content: '节目包括讨论。', 293 | mwb_lc_cbs: '《作见证》第20章8-12段,以及161页的附栏', 294 | mwb_lc_cbs_title: '8.会众研经班', 295 | mwb_song_conclude: 58, 296 | }, 297 | ]; 298 | -------------------------------------------------------------------------------- /test/fixtures/mwb_CH_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | mwb_week_date: '2024/11/04', 4 | mwb_week_date_locale: '11月4-10日', 5 | mwb_weekly_bible_reading: '詩篇105篇', 6 | mwb_song_first: 3, 7 | mwb_tgw_talk: '「他記得他的誓約,直到萬世」', 8 | mwb_tgw_talk_title: '1.「他記得他的誓約,直到萬世」', 9 | mwb_tgw_gems_title: '2.經文寶石', 10 | mwb_tgw_bread: '詩105:24-45(《教導》第5課)', 11 | mwb_tgw_bread_title: '3.經文朗讀', 12 | mwb_ayf_count: 4, 13 | mwb_ayf_part1: '挨家挨戶。對方很忙。(《愛心》第2課第5點)', 14 | mwb_ayf_part1_time: 1, 15 | mwb_ayf_part1_type: '開始交談', 16 | mwb_ayf_part1_title: '4.開始交談', 17 | mwb_ayf_part2: '挨家挨戶。對方想要爭論,你客氣地結束談話。(《愛心》第4課第5點)', 18 | mwb_ayf_part2_time: 2, 19 | mwb_ayf_part2_type: '開始交談', 20 | mwb_ayf_part2_title: '5.開始交談', 21 | mwb_ayf_part3: '挨家挨戶。上次對方對某個話題感興趣,這次你給他看一本相關的雜誌。(《愛心》第8課第3點)', 22 | mwb_ayf_part3_time: 4, 23 | mwb_ayf_part3_type: '繼續幫助對方', 24 | mwb_ayf_part3_title: '6.繼續幫助對方', 25 | mwb_ayf_part4: '非正式見證。向對方介紹JW Library® app,幫助他下載。(《愛心》第9課第5點)', 26 | mwb_ayf_part4_time: 4, 27 | mwb_ayf_part4_type: '繼續幫助對方', 28 | mwb_ayf_part4_title: '7.繼續幫助對方', 29 | mwb_song_middle: 84, 30 | mwb_lc_count: 1, 31 | mwb_lc_part1: '獻出你的愛', 32 | mwb_lc_part1_time: 15, 33 | mwb_lc_part1_title: '8.獻出你的愛', 34 | mwb_lc_part1_content: '節目包括討論。', 35 | mwb_lc_cbs: '《作見證》第17章13-19段', 36 | mwb_lc_cbs_title: '9.會眾研經班', 37 | mwb_song_conclude: 97, 38 | }, 39 | { 40 | mwb_week_date: '2024/11/11', 41 | mwb_week_date_locale: '11月11-17日', 42 | mwb_weekly_bible_reading: '詩篇106篇', 43 | mwb_song_first: 36, 44 | mwb_tgw_talk: '「他們忘記了上帝他們的救主」', 45 | mwb_tgw_talk_title: '1.「他們忘記了上帝他們的救主」', 46 | mwb_tgw_gems_title: '2.經文寶石', 47 | mwb_tgw_bread: '詩106:21-48(《教導》第10課)', 48 | mwb_tgw_bread_title: '3.經文朗讀', 49 | mwb_ayf_count: 2, 50 | mwb_ayf_part1: '節目包括討論。先觀看短片,然後討論《愛心》第11課1-2點。', 51 | mwb_ayf_part1_time: 7, 52 | mwb_ayf_part1_type: '簡單明瞭——耶穌怎麼做', 53 | mwb_ayf_part1_title: '4.簡單明瞭——耶穌怎麼做', 54 | mwb_ayf_part2: '討論《愛心》第11課3-5點,以及「請看」的經文。', 55 | mwb_ayf_part2_time: 8, 56 | mwb_ayf_part2_type: '簡單明瞭——向耶穌學習', 57 | mwb_ayf_part2_title: '5.簡單明瞭——向耶穌學習', 58 | mwb_song_middle: 78, 59 | mwb_lc_count: 1, 60 | mwb_lc_part1: '本地需要', 61 | mwb_lc_part1_time: 15, 62 | mwb_lc_part1_title: '6.本地需要', 63 | mwb_lc_cbs: '《作見證》第18章1-5段,以及142和144頁的附欄', 64 | mwb_lc_cbs_title: '7.會眾研經班', 65 | mwb_song_conclude: 77, 66 | }, 67 | { 68 | mwb_week_date: '2024/11/18', 69 | mwb_week_date_locale: '11月18-24日', 70 | mwb_weekly_bible_reading: '詩篇107-108篇', 71 | mwb_song_first: 7, 72 | mwb_tgw_talk: '「要感謝耶和華,因為他確實良善」', 73 | mwb_tgw_talk_title: '1.「要感謝耶和華,因為他確實良善」', 74 | mwb_tgw_gems_title: '2.經文寶石', 75 | mwb_tgw_bread: '詩107:1-28(《教導》第5課)', 76 | mwb_tgw_bread_title: '3.經文朗讀', 77 | mwb_ayf_count: 3, 78 | mwb_ayf_part1: '非正式見證。(《愛心》第1課第4點)', 79 | mwb_ayf_part1_time: 3, 80 | mwb_ayf_part1_type: '開始交談', 81 | mwb_ayf_part1_title: '4.開始交談', 82 | mwb_ayf_part2: '非正式見證。介紹聖經課程,給對方一張報名學習聖經的名片。(《愛心》第9課第3點)', 83 | mwb_ayf_part2_time: 4, 84 | mwb_ayf_part2_type: '繼續幫助對方', 85 | mwb_ayf_part2_title: '5.繼續幫助對方', 86 | mwb_ayf_part3: '《青年人》第90篇——主題:我的想法總是很消極,怎麼辦?(《教導》第14課)', 87 | mwb_ayf_part3_time: 5, 88 | mwb_ayf_part3_type: '演講', 89 | mwb_ayf_part3_title: '6.演講', 90 | mwb_song_middle: 46, 91 | mwb_lc_count: 1, 92 | mwb_lc_part1: '用歌聲感謝耶和華', 93 | mwb_lc_part1_time: 15, 94 | mwb_lc_part1_title: '7.用歌聲感謝耶和華', 95 | mwb_lc_part1_content: '節目包括討論。', 96 | mwb_lc_cbs: '《作見證》第18章6-15段', 97 | mwb_lc_cbs_title: '8.會眾研經班', 98 | mwb_song_conclude: 73, 99 | }, 100 | { 101 | mwb_week_date: '2024/11/25', 102 | mwb_week_date_locale: '11月25日~12月1日', 103 | mwb_weekly_bible_reading: '詩篇109-112篇', 104 | mwb_song_first: 14, 105 | mwb_tgw_talk: '擁護我們的君王耶穌!', 106 | mwb_tgw_talk_title: '1.擁護我們的君王耶穌!', 107 | mwb_tgw_gems_title: '2.經文寶石', 108 | mwb_tgw_bread: '詩109:1-26(《教導》第2課)', 109 | mwb_tgw_bread_title: '3.經文朗讀', 110 | mwb_ayf_count: 3, 111 | mwb_ayf_part1: '挨家挨戶。用一張傳單的內容開始交談。(《愛心》第4課第3點)', 112 | mwb_ayf_part1_time: 2, 113 | mwb_ayf_part1_type: '開始交談', 114 | mwb_ayf_part1_title: '4.開始交談', 115 | mwb_ayf_part2: '示範。《常見問題》第23篇——主題:為什麼耶和華見證人不參戰?(《愛心》第4課第4點)', 116 | mwb_ayf_part2_time: 5, 117 | mwb_ayf_part2_type: '解釋自己的信仰', 118 | mwb_ayf_part2_title: '5.解釋自己的信仰', 119 | mwb_ayf_part3: '《美好生命》第15課第6點,以及「如果有人說」(《愛心》第11課第3點)', 120 | mwb_ayf_part3_time: 5, 121 | mwb_ayf_part3_type: '教導人成為門徒', 122 | mwb_ayf_part3_title: '6.教導人成為門徒', 123 | mwb_song_middle: 72, 124 | mwb_lc_count: 1, 125 | mwb_lc_part1: '支持王國就該怎麼做?', 126 | mwb_lc_part1_time: 15, 127 | mwb_lc_part1_title: '7.支持王國就該怎麼做?', 128 | mwb_lc_part1_content: '節目包括討論。', 129 | mwb_lc_cbs: '《作見證》第18章16-24段', 130 | mwb_lc_cbs_title: '8.會眾研經班', 131 | mwb_song_conclude: 75, 132 | }, 133 | { 134 | mwb_week_date: '2024/12/02', 135 | mwb_week_date_locale: '12月2-8日', 136 | mwb_weekly_bible_reading: '詩篇113-118篇', 137 | mwb_song_first: 127, 138 | mwb_tgw_talk: '拿什麼報答耶和華?', 139 | mwb_tgw_talk_title: '1.拿什麼報答耶和華?', 140 | mwb_tgw_gems_title: '2.經文寶石', 141 | mwb_tgw_bread: '詩116:1-117:2(《教導》第2課)', 142 | mwb_tgw_bread_title: '3.經文朗讀', 143 | mwb_ayf_count: 2, 144 | mwb_ayf_part1: '節目包括討論。先觀看短片,然後討論《愛心》第12課1-2點。', 145 | mwb_ayf_part1_time: 7, 146 | mwb_ayf_part1_type: '坦率——耶穌怎麼做', 147 | mwb_ayf_part1_title: '4.坦率——耶穌怎麼做', 148 | mwb_ayf_part2: '討論《愛心》第12課3-5點,以及「請看」的經文。', 149 | mwb_ayf_part2_time: 8, 150 | mwb_ayf_part2_type: '坦率——向耶穌學習', 151 | mwb_ayf_part2_title: '5.坦率——向耶穌學習', 152 | mwb_song_middle: 60, 153 | mwb_lc_count: 1, 154 | mwb_lc_part1: '本地需要', 155 | mwb_lc_part1_time: 15, 156 | mwb_lc_part1_title: '6.本地需要', 157 | mwb_lc_cbs: '《作見證》第19章1-5段,以及149-150頁的附欄', 158 | mwb_lc_cbs_title: '7.會眾研經班', 159 | mwb_song_conclude: 29, 160 | }, 161 | { 162 | mwb_week_date: '2024/12/09', 163 | mwb_week_date_locale: '12月9-15日', 164 | mwb_weekly_bible_reading: '詩篇119:1-56', 165 | mwb_song_first: 124, 166 | mwb_tgw_talk: '「年輕人怎樣才能有純潔的行為呢?」', 167 | mwb_tgw_talk_title: '1.「年輕人怎樣才能有純潔的行為呢?」', 168 | mwb_tgw_gems_title: '2.經文寶石', 169 | mwb_tgw_bread: '詩119:1-32(《教導》第5課)', 170 | mwb_tgw_bread_title: '3.經文朗讀', 171 | mwb_ayf_count: 3, 172 | mwb_ayf_part1: '非正式見證。挨家挨戶傳道時,跟街上遇到的行人聊天。(《愛心》第1課第4點)', 173 | mwb_ayf_part1_time: 3, 174 | mwb_ayf_part1_type: '開始交談', 175 | mwb_ayf_part1_title: '4.開始交談', 176 | mwb_ayf_part2: '挨家挨戶。上次對方說,他的家人最近去世了。(《愛心》第9課第3點)', 177 | mwb_ayf_part2_time: 4, 178 | mwb_ayf_part2_type: '繼續幫助對方', 179 | mwb_ayf_part2_title: '5.繼續幫助對方', 180 | mwb_ayf_part3: '《青年人》第83篇——主題:我怎樣才能抗拒誘惑?(《教導》第20課)', 181 | mwb_ayf_part3_time: 5, 182 | mwb_ayf_part3_type: '演講', 183 | mwb_ayf_part3_title: '6.演講', 184 | mwb_song_middle: 40, 185 | mwb_lc_count: 2, 186 | mwb_lc_part1: '12 月的《組織成就了什麼事》', 187 | mwb_lc_part1_time: 10, 188 | mwb_lc_part1_title: '7.12 月的《組織成就了什麼事》', 189 | mwb_lc_part1_content: '播放短片。', 190 | mwb_lc_part2: '本地需要', 191 | mwb_lc_part2_time: 5, 192 | mwb_lc_part2_title: '8.本地需要', 193 | mwb_lc_cbs: '《作見證》第19章6-13段', 194 | mwb_lc_cbs_title: '9.會眾研經班', 195 | mwb_song_conclude: 21, 196 | }, 197 | { 198 | mwb_week_date: '2024/12/16', 199 | mwb_week_date_locale: '12月16-22日', 200 | mwb_weekly_bible_reading: '詩篇119:57-120', 201 | mwb_song_first: 129, 202 | mwb_tgw_talk: '怎樣忍受考驗', 203 | mwb_tgw_talk_title: '1.怎樣忍受考驗', 204 | mwb_tgw_gems_title: '2.經文寶石', 205 | mwb_tgw_bread: '詩119:57-80(《教導》第12課)', 206 | mwb_tgw_bread_title: '3.經文朗讀', 207 | mwb_ayf_count: 3, 208 | mwb_ayf_part1: '挨家挨戶。給對方看我們的網站,並給他一張JW網站名片。(《愛心》第2課第5點)', 209 | mwb_ayf_part1_time: 3, 210 | mwb_ayf_part1_type: '開始交談', 211 | mwb_ayf_part1_title: '4.開始交談', 212 | mwb_ayf_part2: 213 | '非正式見證。邀請對方來聽下週的公眾演講,給他看短片《王國聚會所的聚會是怎樣的?》。(《愛心》第8課第3點)', 214 | mwb_ayf_part2_time: 4, 215 | mwb_ayf_part2_type: '繼續幫助對方', 216 | mwb_ayf_part2_title: '5.繼續幫助對方', 217 | mwb_ayf_part3: '示範。《聖經問答》第157篇——主題:關於自然災害,聖經怎麼說?(《愛心》第3課第3點)', 218 | mwb_ayf_part3_time: 5, 219 | mwb_ayf_part3_type: '解釋自己的信仰', 220 | mwb_ayf_part3_title: '6.解釋自己的信仰', 221 | mwb_song_middle: 128, 222 | mwb_lc_count: 1, 223 | mwb_lc_part1: '耶和華會幫助我們忍耐下去', 224 | mwb_lc_part1_time: 15, 225 | mwb_lc_part1_title: '7.耶和華會幫助我們忍耐下去', 226 | mwb_lc_part1_content: '節目包括討論。', 227 | mwb_lc_cbs: '《作見證》第19章14-20段,以及152頁的附欄', 228 | mwb_lc_cbs_title: '8.會眾研經班', 229 | mwb_song_conclude: 32, 230 | }, 231 | { 232 | mwb_week_date: '2024/12/23', 233 | mwb_week_date_locale: '12月23-29日', 234 | mwb_weekly_bible_reading: '詩篇119:121-176', 235 | mwb_song_first: 31, 236 | mwb_tgw_talk: '有些痛苦是可以避免的', 237 | mwb_tgw_talk_title: '1.有些痛苦是可以避免的', 238 | mwb_tgw_gems_title: '2.經文寶石', 239 | mwb_tgw_bread: '詩119:121-152(《教導》第2課)', 240 | mwb_tgw_bread_title: '3.經文朗讀', 241 | mwb_ayf_count: 3, 242 | mwb_ayf_part1: '挨家挨戶。(《愛心》第1課第5點)', 243 | mwb_ayf_part1_time: 3, 244 | mwb_ayf_part1_type: '開始交談', 245 | mwb_ayf_part1_title: '4.開始交談', 246 | mwb_ayf_part2: '挨家挨戶。告訴對方怎樣在jw.org上找到他感興趣的內容。(《愛心》第8課第3點)', 247 | mwb_ayf_part2_time: 4, 248 | mwb_ayf_part2_type: '繼續幫助對方', 249 | mwb_ayf_part2_title: '5.繼續幫助對方', 250 | mwb_ayf_part3: '學生不經常參加聚會,跟他談談這個問題。(《愛心》第12課第4點)', 251 | mwb_ayf_part3_time: 5, 252 | mwb_ayf_part3_type: '教導人成為門徒', 253 | mwb_ayf_part3_title: '6.教導人成為門徒', 254 | mwb_song_middle: 121, 255 | mwb_lc_count: 1, 256 | mwb_lc_part1: '不要因財「受傷」', 257 | mwb_lc_part1_time: 15, 258 | mwb_lc_part1_title: '7.不要因財「受傷」', 259 | mwb_lc_part1_content: '節目包括討論。', 260 | mwb_lc_cbs: '《作見證》第7部分引言,以及第20章1-7段', 261 | mwb_lc_cbs_title: '8.會眾研經班', 262 | mwb_song_conclude: 101, 263 | }, 264 | { 265 | mwb_week_date: '2024/12/30', 266 | mwb_week_date_locale: '2024年12月30日~2025年1月5日', 267 | mwb_weekly_bible_reading: '詩篇120-126篇', 268 | mwb_song_first: 144, 269 | mwb_tgw_talk: '他們流淚撒種,歡呼收割', 270 | mwb_tgw_talk_title: '1.他們流淚撒種,歡呼收割', 271 | mwb_tgw_gems_title: '2.經文寶石', 272 | mwb_tgw_bread: '詩124:1-126:6(《教導》第5課)', 273 | mwb_tgw_bread_title: '3.經文朗讀', 274 | mwb_ayf_count: 3, 275 | mwb_ayf_part1: '公眾場所見證。(《愛心》第3課第5點)', 276 | mwb_ayf_part1_time: 3, 277 | mwb_ayf_part1_type: '開始交談', 278 | mwb_ayf_part1_title: '4.開始交談', 279 | mwb_ayf_part2: '挨家挨戶。上次對方說聖經不切實際。(《愛心》第9課第5點)', 280 | mwb_ayf_part2_time: 4, 281 | mwb_ayf_part2_type: '繼續幫助對方', 282 | mwb_ayf_part2_title: '5.繼續幫助對方', 283 | mwb_ayf_part3: '《美好生命》第16課引言,以及1-3點(《愛心》第11課第3點)', 284 | mwb_ayf_part3_time: 5, 285 | mwb_ayf_part3_type: '教導人成為門徒', 286 | mwb_ayf_part3_title: '6.教導人成為門徒', 287 | mwb_song_middle: 155, 288 | mwb_lc_count: 1, 289 | mwb_lc_part1: '因上帝的承諾而快樂', 290 | mwb_lc_part1_time: 15, 291 | mwb_lc_part1_title: '7.因上帝的承諾而快樂', 292 | mwb_lc_part1_content: '節目包括討論。', 293 | mwb_lc_cbs: '《作見證》第20章8-12段,以及161頁的附欄', 294 | mwb_lc_cbs_title: '8.會眾研經班', 295 | mwb_song_conclude: 58, 296 | }, 297 | ]; 298 | -------------------------------------------------------------------------------- /test/fixtures/mwb_D_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | mwb_week_date: '4.-10. NOVEMBER', 4 | mwb_weekly_bible_reading: 'SALME 105', 5 | mwb_song_first: 3, 6 | mwb_tgw_talk: '1. “Han husker for evigt sin pagt” (10 min.)', 7 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 8 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 105:24-45 (th arbejdspunkt 5)', 9 | mwb_ayf_count: 4, 10 | mwb_ayf_part1: '4. Start en samtale (1 min.) FRA HUS TIL HUS. Den besøgte har travlt. (lmd lektion 2 punkt 5)', 11 | mwb_ayf_part2: 12 | '5. Start en samtale (2 min.) FRA HUS TIL HUS. Du afslutter samtalen på en venlig måde da den besøgte vil diskutere. (lmd lektion 4 punkt 5)', 13 | mwb_ayf_part3: 14 | '6. Følg op på interessen (4 min.) FRA HUS TIL HUS. Tilbyd et blad om noget som den besøgte tidligere har udtrykt interesse for. (lmd lektion 8 punkt 3)', 15 | mwb_ayf_part4: 16 | '7. Følg op på interessen (4 min.) UFORMEL FORKYNDELSE. Fortæl personen om JW Library®-appen, og hjælp ham eller hende med at downloade den. (lmd lektion 9 punkt 5)', 17 | mwb_song_middle: 84, 18 | mwb_lc_count: 1, 19 | mwb_lc_part1: '8. Hvordan vi kan vise vores kærlighed (15 min.) Drøftelse.', 20 | mwb_lc_cbs: '9. Menighedsbibelstudiet (30 min.) bt kap. 17 § 13-19', 21 | mwb_song_conclude: 97, 22 | }, 23 | { 24 | mwb_week_date: '11.-17. NOVEMBER', 25 | mwb_weekly_bible_reading: 'SALME 106', 26 | mwb_song_first: 36, 27 | mwb_tgw_talk: '1. “De glemte Gud, deres Frelser” (10 min.)', 28 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 29 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 106:21-48 (th arbejdspunkt 10)', 30 | mwb_ayf_count: 2, 31 | mwb_ayf_part1: 32 | '4. Hold det enkelt – Hvad Jesus gjorde (7 min.) Drøftelse. Afspil VIDEOEN, og drøft derefter Elsk mennesker, lektion 11, punkt 1-2.', 33 | mwb_ayf_part2: 34 | '5. Hold det enkelt – Efterlign Jesus (8 min.) Drøftelse baseret på Elsk mennesker, lektion 11, punkt 3-5 og “Se også”.', 35 | mwb_song_middle: 78, 36 | mwb_lc_count: 1, 37 | mwb_lc_part1: '6. Tilrettelægges lokalt (15 min.)', 38 | mwb_lc_cbs: '7. Menighedsbibelstudiet (30 min.) bt kap. 18 § 1-5, bokse på s. 142, 144', 39 | mwb_song_conclude: 77, 40 | }, 41 | { 42 | mwb_week_date: '18.-24. NOVEMBER', 43 | mwb_weekly_bible_reading: 'SALME 107-108', 44 | mwb_song_first: 7, 45 | mwb_tgw_talk: '1. “Tak Jehova, for han er god” (10 min.)', 46 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 47 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 107:1-28 (th arbejdspunkt 5)', 48 | mwb_ayf_count: 3, 49 | mwb_ayf_part1: '4. Start en samtale (3 min.) UFORMEL FORKYNDELSE. (lmd lektion 1 punkt 4)', 50 | mwb_ayf_part2: 51 | '5. Følg op på interessen (4 min.) UFORMEL FORKYNDELSE. Fortæl om det bibelkursus vi tilbyder, og giv den besøgte visitkortet “Gratis bibelkursus”. (lmd lektion 9 punkt 3)', 52 | mwb_ayf_part3: '6. Foredrag (5 min.) ijwyp 90 – Tema: Hvordan kan jeg undgå negative tanker? (th arbejdspunkt 14)', 53 | mwb_song_middle: 46, 54 | mwb_lc_count: 1, 55 | mwb_lc_part1: '7. Vi synger for at takke Jehova (15 min.) Drøftelse.', 56 | mwb_lc_cbs: '8. Menighedsbibelstudiet (30 min.) bt kap. 18 § 6-15', 57 | mwb_song_conclude: 73, 58 | }, 59 | { 60 | mwb_week_date: '25. NOVEMBER – 1. DECEMBER', 61 | mwb_weekly_bible_reading: 'SALME 109-112', 62 | mwb_song_first: 14, 63 | mwb_tgw_talk: '1. Støt vores konge, Jesus (10 min.)', 64 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 65 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 109:1-26 (th arbejdspunkt 2)', 66 | mwb_ayf_count: 3, 67 | mwb_ayf_part1: 68 | '4. Start en samtale (2 min.) FRA HUS TIL HUS. Start en samtale ved hjælp af en folder. (lmd lektion 4 punkt 3)', 69 | mwb_ayf_part2: 70 | '5. Fortæl om din tro (5 min.) Demonstration. ijwfq 23 – Tema: Hvorfor går Jehovas Vidner ikke i krig? (lmd lektion 4 punkt 4)', 71 | mwb_ayf_part3: 72 | '6. Hjælp eleven til at gøre fremskridt (5 min.) lff lektion 15 punkt 6 og Nogle siger måske (lmd lektion 11 punkt 3)', 73 | mwb_song_middle: 72, 74 | mwb_lc_count: 1, 75 | mwb_lc_part1: '7. Hvordan kan vi loyalt støtte Riget? (15 min.) Drøftelse.', 76 | mwb_lc_cbs: '8. Menighedsbibelstudiet (30 min.) bt kap. 18 § 16-24', 77 | mwb_song_conclude: 75, 78 | }, 79 | { 80 | mwb_week_date: '2.-8. DECEMBER', 81 | mwb_weekly_bible_reading: 'SALME 113-118', 82 | mwb_song_first: 127, 83 | mwb_tgw_talk: '1. Hvordan skal vi betale Jehova tilbage? (10 min.)', 84 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 85 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 116:1 – 117:2 (th arbejdspunkt 2)', 86 | mwb_ayf_count: 2, 87 | mwb_ayf_part1: 88 | '4. Sig det der skal siges – Hvad Jesus gjorde (7 min.) Drøftelse. Afspil VIDEOEN, og drøft derefter Elsk mennesker, lektion 12, punkt 1-2.', 89 | mwb_ayf_part2: 90 | '5. Sig det der skal siges – Efterlign Jesus (8 min.) Drøftelse baseret på Elsk mennesker, lektion 12, punkt 3-5 og “Se også”.', 91 | mwb_song_middle: 60, 92 | mwb_lc_count: 1, 93 | mwb_lc_part1: '6. Tilrettelægges lokalt (15 min.)', 94 | mwb_lc_cbs: '7. Menighedsbibelstudiet (30 min.) bt kap. 19 § 1-5, bokse på s. 149-150', 95 | mwb_song_conclude: 29, 96 | }, 97 | { 98 | mwb_week_date: '9.-15. DECEMBER', 99 | mwb_weekly_bible_reading: 'SALME 119:1-56', 100 | mwb_song_first: 124, 101 | mwb_tgw_talk: '1. “Hvordan holder en ung mand sin sti ren?” (10 min.)', 102 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 103 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 119:1-32 (th arbejdspunkt 5)', 104 | mwb_ayf_count: 3, 105 | mwb_ayf_part1: 106 | '4. Start en samtale (3 min.) UFORMEL FORKYNDELSE. Start en samtale med en du møder mellem besøgene i hus til hus-forkyndelsen. (lmd lektion 1 punkt 4)', 107 | mwb_ayf_part2: 108 | '5. Følg op på interessen (4 min.) FRA HUS TIL HUS. I en foregående samtale har du fået at vide at personen for nylig har mistet en af sine kære. (lmd lektion 9 punkt 3)', 109 | mwb_ayf_part3: '6. Foredrag (5 min.) ijwyp 83 – Tema: Hvordan kan jeg modstå fristelser? (th arbejdspunkt 20)', 110 | mwb_song_middle: 40, 111 | mwb_lc_count: 2, 112 | mwb_lc_part1: '7. Hvad organisationen udretter for december (10 min.) Afspil VIDEOEN.', 113 | mwb_lc_part2: '8. Tilrettelægges lokalt (5 min.)', 114 | mwb_lc_cbs: '9. Menighedsbibelstudiet (30 min.) bt kap. 19 § 6-13', 115 | mwb_song_conclude: 21, 116 | }, 117 | { 118 | mwb_week_date: '16.-22. DECEMBER', 119 | mwb_weekly_bible_reading: 'SALME 119:57-120', 120 | mwb_song_first: 129, 121 | mwb_tgw_talk: '1. Hvordan man holder ud i svære tider (10 min.)', 122 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 123 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 119:57-80 (th arbejdspunkt 12)', 124 | mwb_ayf_count: 3, 125 | mwb_ayf_part1: 126 | '4. Start en samtale (3 min.) FRA HUS TIL HUS. Vis vores hjemmeside, og giv den besøgte et visitkort til jw.org. (lmd lektion 2 punkt 5)', 127 | mwb_ayf_part2: 128 | '5. Følg op på interessen (4 min.) UFORMEL FORKYNDELSE. Invitér den besøgte til det næste offentlige foredrag. Vis og tal om videoen (uden at afspille) Hvad sker der i en rigssal? (lmd lektion 8 punkt 3)', 129 | mwb_ayf_part3: 130 | '6. Fortæl om din tro (5 min.) Demonstration. ijwbq 157 – Tema: Hvad siger Bibelen om naturkatastrofer? (lmd lektion 3 punkt 3)', 131 | mwb_song_middle: 128, 132 | mwb_lc_count: 1, 133 | mwb_lc_part1: '7. Jehova hjælper os til at holde ud (15 min.) Drøftelse.', 134 | mwb_lc_cbs: '8. Menighedsbibelstudiet (30 min.) bt kap. 19 § 14-20, boks på s. 152', 135 | mwb_song_conclude: 32, 136 | }, 137 | { 138 | mwb_week_date: '23.-29. DECEMBER', 139 | mwb_weekly_bible_reading: 'SALME 119:121-176', 140 | mwb_song_first: 31, 141 | mwb_tgw_talk: '1. Hvordan man skåner sig selv for konsekvenserne af dårlige beslutninger (10 min.)', 142 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 143 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 119:121-152 (th arbejdspunkt 2)', 144 | mwb_ayf_count: 3, 145 | mwb_ayf_part1: '4. Start en samtale (3 min.) FRA HUS TIL HUS. (lmd lektion 1 punkt 5)', 146 | mwb_ayf_part2: 147 | '5. Følg op på interessen (4 min.) FRA HUS TIL HUS. Vis den besøgte hvordan man kan finde oplysninger på jw.org som kunne interessere ham eller hende. (lmd lektion 8 punkt 3)', 148 | mwb_ayf_part3: 149 | '6. Hjælp eleven til at gøre fremskridt (5 min.) Samtale med en bibelstudieelev der ikke kommer fast til møderne. (lmd lektion 12 punkt 4)', 150 | mwb_song_middle: 121, 151 | mwb_lc_count: 1, 152 | mwb_lc_part1: '7. Lad ikke penge være årsag til smerte (15 min.) Drøftelse.', 153 | mwb_lc_cbs: '8. Menighedsbibelstudiet (30 min.) bt kap. 20 § 1-7 og indledning til sektion 7', 154 | mwb_song_conclude: 101, 155 | }, 156 | { 157 | mwb_week_date: '30. DECEMBER 2024 – 5. JANUAR 2025', 158 | mwb_weekly_bible_reading: 'SALME 120-126', 159 | mwb_song_first: 144, 160 | mwb_tgw_talk: '1. De såede med tårer, men høstede med glæde (10 min.)', 161 | mwb_tgw_gems_title: '2. Åndelige perler (10 min.)', 162 | mwb_tgw_bread: '3. Oplæsning fra Bibelen (4 min.) Sl 124:1 – 126:6 (th arbejdspunkt 5)', 163 | mwb_ayf_count: 3, 164 | mwb_ayf_part1: '4. Start en samtale (3 min.) OFFENTLIG FORKYNDELSE. (lmd lektion 3 punkt 5)', 165 | mwb_ayf_part2: 166 | '5. Følg op på interessen (4 min.) FRA HUS TIL HUS. Sidste gang sagde den besøgte at han tvivlede på at Bibelen er sand. (lmd lektion 9 punkt 5)', 167 | mwb_ayf_part3: 168 | '6. Hjælp eleven til at gøre fremskridt (5 min.) lff lektion 16 indledning og punkt 1-3 (lmd lektion 11 punkt 3)', 169 | mwb_song_middle: 155, 170 | mwb_lc_count: 1, 171 | mwb_lc_part1: '7. Glæd dig over Jehovas løfter (15 min.) Drøftelse.', 172 | mwb_lc_cbs: '8. Menighedsbibelstudiet (30 min.) bt kap. 20 § 8-12, boks på s. 161', 173 | mwb_song_conclude: 58, 174 | }, 175 | ]; 176 | -------------------------------------------------------------------------------- /test/fixtures/mwb_J_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | mwb_week_date: '2024/11/04', 4 | mwb_week_date_locale: '11月4-10日', 5 | mwb_weekly_bible_reading: '詩編 105編', 6 | mwb_song_first: 3, 7 | mwb_tgw_talk: '「神は覚えている。ご自分の契約を永遠に」', 8 | mwb_tgw_talk_title: '1. 「神は覚えている。ご自分の契約を永遠に」', 9 | mwb_tgw_gems_title: '2. 宝石を探し出す', 10 | mwb_tgw_bread: '105:24-45(教励 第5課)', 11 | mwb_tgw_bread_title: '3. 聖書朗読', 12 | mwb_ayf_count: 4, 13 | mwb_ayf_part1: '家から家で。家の人が忙しくしている。(愛込 レッスン2 ポイント5)', 14 | mwb_ayf_part1_time: 1, 15 | mwb_ayf_part1_type: '会話を始める', 16 | mwb_ayf_part1_title: '4. 会話を始める', 17 | mwb_ayf_part2: 18 | '家から家で。相手の口調が攻撃的になってきたら,穏やかに会話を終わらせる。(愛込 レッスン4 ポイント5)', 19 | mwb_ayf_part2_time: 2, 20 | mwb_ayf_part2_type: '会話を始める', 21 | mwb_ayf_part2_title: '5. 会話を始める', 22 | mwb_ayf_part3: '家から家で。家の人が前回の訪問で関心を示したテーマの雑誌を紹介する。(愛込 レッスン8 ポイント3)', 23 | mwb_ayf_part3_time: 4, 24 | mwb_ayf_part3_type: '再び話し合う', 25 | mwb_ayf_part3_title: '6. 再び話し合う', 26 | mwb_ayf_part4: 27 | '日常生活で。JW Library®アプリについて話し,アプリをダウンロードするのを手伝う。(愛込 レッスン9 ポイント5)', 28 | mwb_ayf_part4_time: 4, 29 | mwb_ayf_part4_type: '再び話し合う', 30 | mwb_ayf_part4_title: '7. 再び話し合う', 31 | mwb_song_middle: 84, 32 | mwb_lc_count: 1, 33 | mwb_lc_part1: 'イエスへの愛を表す', 34 | mwb_lc_part1_time: 15, 35 | mwb_lc_part1_title: '8. イエスへの愛を表す', 36 | mwb_lc_part1_content: '討議。', 37 | mwb_lc_cbs: '17章13-19節', 38 | mwb_lc_cbs_title: '9. 会衆の聖書研究', 39 | mwb_song_conclude: 97, 40 | }, 41 | { 42 | mwb_week_date: '2024/11/11', 43 | mwb_week_date_locale: '11月11-17日', 44 | mwb_weekly_bible_reading: '詩編 106編', 45 | mwb_song_first: 36, 46 | mwb_tgw_talk: '「救い主である神を忘れた」', 47 | mwb_tgw_talk_title: '1. 「救い主である神を忘れた」', 48 | mwb_tgw_gems_title: '2. 宝石を探し出す', 49 | mwb_tgw_bread: '106:21-48(教励 第10課)', 50 | mwb_tgw_bread_title: '3. 聖書朗読', 51 | mwb_ayf_count: 2, 52 | mwb_ayf_part1: '討議。動画を再生する。愛込 レッスン11 ポイント1-2を話し合う。', 53 | mwb_ayf_part1_time: 7, 54 | mwb_ayf_part1_type: '分かりやすく: イエスの手本', 55 | mwb_ayf_part1_title: '4. 分かりやすく: イエスの手本', 56 | mwb_ayf_part2: '愛込 レッスン11 ポイント3-5,「参考になる他の聖句」に基づく討議。', 57 | mwb_ayf_part2_time: 8, 58 | mwb_ayf_part2_type: '分かりやすく: イエスに見習う', 59 | mwb_ayf_part2_title: '5. 分かりやすく: イエスに見習う', 60 | mwb_song_middle: 78, 61 | mwb_lc_count: 1, 62 | mwb_lc_part1: '会衆の必要', 63 | mwb_lc_part1_time: 15, 64 | mwb_lc_part1_title: '6. 会衆の必要', 65 | mwb_lc_cbs: '18章1-5節,142,144ページの囲み', 66 | mwb_lc_cbs_title: '7. 会衆の聖書研究', 67 | mwb_song_conclude: 77, 68 | }, 69 | { 70 | mwb_week_date: '2024/11/18', 71 | mwb_week_date_locale: '11月18-24日', 72 | mwb_weekly_bible_reading: '詩編 107-108編', 73 | mwb_song_first: 7, 74 | mwb_tgw_talk: '「エホバに感謝せよ。神は善い方」', 75 | mwb_tgw_talk_title: '1. 「エホバに感謝せよ。神は善い方」', 76 | mwb_tgw_gems_title: '2. 宝石を探し出す', 77 | mwb_tgw_bread: '107:1-28(教励 第5課)', 78 | mwb_tgw_bread_title: '3. 聖書朗読', 79 | mwb_ayf_count: 3, 80 | mwb_ayf_part1: '日常生活で。(愛込 レッスン1 ポイント4)', 81 | mwb_ayf_part1_time: 3, 82 | mwb_ayf_part1_type: '会話を始める', 83 | mwb_ayf_part1_title: '4. 会話を始める', 84 | mwb_ayf_part2: 85 | '日常生活で。聖書レッスンについて話し,聖書レッスン紹介用コンタクトカードを渡す。(愛込 レッスン9 ポイント3)', 86 | mwb_ayf_part2_time: 4, 87 | mwb_ayf_part2_type: '再び話し合う', 88 | mwb_ayf_part2_title: '5. 再び話し合う', 89 | mwb_ayf_part3: 'イ尋 90 主題: マイナス思考をやめるには。(教励 第14課)', 90 | mwb_ayf_part3_time: 5, 91 | mwb_ayf_part3_type: '話', 92 | mwb_ayf_part3_title: '6. 話', 93 | mwb_song_middle: 46, 94 | mwb_lc_count: 1, 95 | mwb_lc_part1: '歌でエホバへの感謝を表す', 96 | mwb_lc_part1_time: 15, 97 | mwb_lc_part1_title: '7. 歌でエホバへの感謝を表す', 98 | mwb_lc_part1_content: '討議。', 99 | mwb_lc_cbs: '18章6-15節', 100 | mwb_lc_cbs_title: '8. 会衆の聖書研究', 101 | mwb_song_conclude: 73, 102 | }, 103 | { 104 | mwb_week_date: '2024/11/25', 105 | mwb_week_date_locale: '11月25日-12月1日', 106 | mwb_weekly_bible_reading: '詩編 109-112編', 107 | mwb_song_first: 14, 108 | mwb_tgw_talk: '王イエスを支持する', 109 | mwb_tgw_talk_title: '1. 王イエスを支持する', 110 | mwb_tgw_gems_title: '2. 宝石を探し出す', 111 | mwb_tgw_bread: '109:1-26(教励 第2課)', 112 | mwb_tgw_bread_title: '3. 聖書朗読', 113 | mwb_ayf_count: 3, 114 | mwb_ayf_part1: '家から家で。パンフレットを使って会話を始める。(愛込 レッスン4 ポイント3)', 115 | mwb_ayf_part1_time: 2, 116 | mwb_ayf_part1_type: '会話を始める', 117 | mwb_ayf_part1_title: '4. 会話を始める', 118 | mwb_ayf_part2: '実演。イ質 23 話題: エホバの証人が戦争に行かないのはなぜですか。(愛込 レッスン4 ポイント4)', 119 | mwb_ayf_part2_time: 5, 120 | mwb_ayf_part2_type: '信じていることを説明する', 121 | mwb_ayf_part2_title: '5. 信じていることを説明する', 122 | mwb_ayf_part3: 'レッスン15 ポイント6と「こう言う人もいる」(愛込 レッスン11 ポイント3)', 123 | mwb_ayf_part3_time: 5, 124 | mwb_ayf_part3_type: '教えて育てる', 125 | mwb_ayf_part3_title: '6. 教えて育てる', 126 | mwb_song_middle: 72, 127 | mwb_lc_count: 1, 128 | mwb_lc_part1: '神の王国を心から支持する方法', 129 | mwb_lc_part1_time: 15, 130 | mwb_lc_part1_title: '7. 神の王国を心から支持する方法', 131 | mwb_lc_part1_content: '討議。', 132 | mwb_lc_cbs: '18章16-24節', 133 | mwb_lc_cbs_title: '8. 会衆の聖書研究', 134 | mwb_song_conclude: 75, 135 | }, 136 | { 137 | mwb_week_date: '2024/12/02', 138 | mwb_week_date_locale: '12月2-8日', 139 | mwb_weekly_bible_reading: '詩編 113-118編', 140 | mwb_song_first: 127, 141 | mwb_tgw_talk: 'エホバにお返しするために何ができるか', 142 | mwb_tgw_talk_title: '1. エホバにお返しするために何ができるか', 143 | mwb_tgw_gems_title: '2. 宝石を探し出す', 144 | mwb_tgw_bread: '116:1–117:2(教励 第2課)', 145 | mwb_tgw_bread_title: '3. 聖書朗読', 146 | mwb_ayf_count: 2, 147 | mwb_ayf_part1: '討議。動画を再生する。愛込 レッスン12 ポイント1-2を話し合う。', 148 | mwb_ayf_part1_time: 7, 149 | mwb_ayf_part1_type: 'しっかり向き合って: イエスの手本', 150 | mwb_ayf_part1_title: '4. しっかり向き合って: イエスの手本', 151 | mwb_ayf_part2: '愛込 レッスン12 ポイント3-5,「参考になる他の聖句」に基づく討議。', 152 | mwb_ayf_part2_time: 8, 153 | mwb_ayf_part2_type: 'しっかり向き合って: イエスに見習う', 154 | mwb_ayf_part2_title: '5. しっかり向き合って: イエスに見習う', 155 | mwb_song_middle: 60, 156 | mwb_lc_count: 1, 157 | mwb_lc_part1: '会衆の必要', 158 | mwb_lc_part1_time: 15, 159 | mwb_lc_part1_title: '6. 会衆の必要', 160 | mwb_lc_cbs: '19章1-5節,149-150ページ囲み', 161 | mwb_lc_cbs_title: '7. 会衆の聖書研究', 162 | mwb_song_conclude: 29, 163 | }, 164 | { 165 | mwb_week_date: '2024/12/09', 166 | mwb_week_date_locale: '12月9-15日', 167 | mwb_weekly_bible_reading: '詩編 119編1-56節', 168 | mwb_song_first: 124, 169 | mwb_tgw_talk: '「若い人はどうすれば清く生きられるだろう」', 170 | mwb_tgw_talk_title: '1. 「若い人はどうすれば清く生きられるだろう」', 171 | mwb_tgw_gems_title: '2. 宝石を探し出す', 172 | mwb_tgw_bread: '119:1-32(教励 第5課)', 173 | mwb_tgw_bread_title: '3. 聖書朗読', 174 | mwb_ayf_count: 3, 175 | mwb_ayf_part1: '家から家で。伝道中に道で会った人と会話を始める。(愛込 レッスン1 ポイント4)', 176 | mwb_ayf_part1_time: 3, 177 | mwb_ayf_part1_type: '会話を始める', 178 | mwb_ayf_part1_title: '4. 会話を始める', 179 | mwb_ayf_part2: '家から家で。前回大切な人を亡くしたばかりだと言っていた人と会話する。(愛込 レッスン9 ポイント3)', 180 | mwb_ayf_part2_time: 4, 181 | mwb_ayf_part2_type: '再び話し合う', 182 | mwb_ayf_part2_title: '5. 再び話し合う', 183 | mwb_ayf_part3: 'イ尋 83 主題: 誘惑に負けそうになったら(教励 第20課)', 184 | mwb_ayf_part3_time: 5, 185 | mwb_ayf_part3_type: '話', 186 | mwb_ayf_part3_title: '6. 話', 187 | mwb_song_middle: 40, 188 | mwb_lc_count: 2, 189 | mwb_lc_part1: '12月の「組織の活動の進展」', 190 | mwb_lc_part1_time: 10, 191 | mwb_lc_part1_title: '7. 12月の「組織の活動の進展」', 192 | mwb_lc_part1_content: '動画を再生する。', 193 | mwb_lc_part2: '会衆の必要', 194 | mwb_lc_part2_time: 5, 195 | mwb_lc_part2_title: '8. 会衆の必要', 196 | mwb_lc_cbs: '19章6-13節', 197 | mwb_lc_cbs_title: '9. 会衆の聖書研究', 198 | mwb_song_conclude: 21, 199 | }, 200 | { 201 | mwb_week_date: '2024/12/16', 202 | mwb_week_date_locale: '12月16-22日', 203 | mwb_weekly_bible_reading: '詩編 119編57-120節', 204 | mwb_song_first: 129, 205 | mwb_tgw_talk: 'つらい状況で忍耐するために', 206 | mwb_tgw_talk_title: '1. つらい状況で忍耐するために', 207 | mwb_tgw_gems_title: '2. 宝石を探し出す', 208 | mwb_tgw_bread: '119:57-80(教励 第12課)', 209 | mwb_tgw_bread_title: '3. 聖書朗読', 210 | mwb_ayf_count: 3, 211 | mwb_ayf_part1: 212 | '家から家で。家の人に公式ウェブサイトを見せ,jw.orgコンタクトカードを渡す。(愛込 レッスン2 ポイント5)', 213 | mwb_ayf_part1_time: 3, 214 | mwb_ayf_part1_type: '会話を始める', 215 | mwb_ayf_part1_title: '4. 会話を始める', 216 | mwb_ayf_part2: 217 | '日常生活で。次回の公開講演に招待する。「王国会館においでください」の動画を紹介する。(愛込 レッスン8 ポイント3)', 218 | mwb_ayf_part2_time: 4, 219 | mwb_ayf_part2_type: '再び話し合う', 220 | mwb_ayf_part2_title: '5. 再び話し合う', 221 | mwb_ayf_part3: '実演。イ聖 157 話題: 聖書は自然災害について何と述べていますか。(愛込 レッスン3 ポイント3)', 222 | mwb_ayf_part3_time: 5, 223 | mwb_ayf_part3_type: '信じていることを説明する', 224 | mwb_ayf_part3_title: '6. 信じていることを説明する', 225 | mwb_song_middle: 128, 226 | mwb_lc_count: 1, 227 | mwb_lc_part1: '忍耐できるようエホバは助けてくれる', 228 | mwb_lc_part1_time: 15, 229 | mwb_lc_part1_title: '7. 忍耐できるようエホバは助けてくれる', 230 | mwb_lc_part1_content: '討議。', 231 | mwb_lc_cbs: '19章14-20節,152ページ囲み', 232 | mwb_lc_cbs_title: '8. 会衆の聖書研究', 233 | mwb_song_conclude: 32, 234 | }, 235 | { 236 | mwb_week_date: '2024/12/23', 237 | mwb_week_date_locale: '12月23-29日', 238 | mwb_weekly_bible_reading: '詩編 119編121-176節', 239 | mwb_song_first: 31, 240 | mwb_tgw_talk: 'つらい思いをしないためにできること', 241 | mwb_tgw_talk_title: '1. つらい思いをしないためにできること', 242 | mwb_tgw_gems_title: '2. 宝石を探し出す', 243 | mwb_tgw_bread: '119:121-152(教励 第2課)', 244 | mwb_tgw_bread_title: '3. 聖書朗読', 245 | mwb_ayf_count: 3, 246 | mwb_ayf_part1: '家から家で。(愛込 レッスン1 ポイント5)', 247 | mwb_ayf_part1_time: 3, 248 | mwb_ayf_part1_type: '会話を始める', 249 | mwb_ayf_part1_title: '4. 会話を始める', 250 | mwb_ayf_part2: '家から家で。関心のある情報をjw.orgで見つける方法を伝える。(愛込 レッスン8 ポイント3)', 251 | mwb_ayf_part2_time: 4, 252 | mwb_ayf_part2_type: '再び話し合う', 253 | mwb_ayf_part2_title: '5. 再び話し合う', 254 | mwb_ayf_part3: 255 | '集会に定期的に出席していないことについて,聖書を学んでいる人と話し合う。(愛込 レッスン12 ポイント4)', 256 | mwb_ayf_part3_time: 5, 257 | mwb_ayf_part3_type: '教えて育てる', 258 | mwb_ayf_part3_title: '6. 教えて育てる', 259 | mwb_song_middle: 121, 260 | mwb_lc_count: 1, 261 | mwb_lc_part1: 'お金を愛すると苦しい思いをする', 262 | mwb_lc_part1_time: 15, 263 | mwb_lc_part1_title: '7. お金を愛すると苦しい思いをする', 264 | mwb_lc_part1_content: '討議。', 265 | mwb_lc_cbs: '20章1-7節,セクション7の紹介', 266 | mwb_lc_cbs_title: '8. 会衆の聖書研究', 267 | mwb_song_conclude: 101, 268 | }, 269 | { 270 | mwb_week_date: '2024/12/30', 271 | mwb_week_date_locale: '2024年12月30日-2025年1月5日', 272 | mwb_weekly_bible_reading: '詩編 120-126編', 273 | mwb_song_first: 144, 274 | mwb_tgw_talk: '泣きながら種をまき,笑顔で収穫する', 275 | mwb_tgw_talk_title: '1. 泣きながら種をまき,笑顔で収穫する', 276 | mwb_tgw_gems_title: '2. 宝石を探し出す', 277 | mwb_tgw_bread: '124:1–126:6(教励 第5課)', 278 | mwb_tgw_bread_title: '3. 聖書朗読', 279 | mwb_ayf_count: 3, 280 | mwb_ayf_part1: '公共エリア伝道。(愛込 レッスン3 ポイント5)', 281 | mwb_ayf_part1_time: 3, 282 | mwb_ayf_part1_type: '会話を始める', 283 | mwb_ayf_part1_title: '4. 会話を始める', 284 | mwb_ayf_part2: 285 | '家から家で。聖書に書かれていることが信用できるか分からない,と前回言った人と会話する。(愛込 レッスン9 ポイント5)', 286 | mwb_ayf_part2_time: 4, 287 | mwb_ayf_part2_type: '再び話し合う', 288 | mwb_ayf_part2_title: '5. 再び話し合う', 289 | mwb_ayf_part3: 'レッスン16 序文とポイント1-3(愛込 レッスン11 ポイント3)', 290 | mwb_ayf_part3_time: 5, 291 | mwb_ayf_part3_type: '教えて育てる', 292 | mwb_ayf_part3_title: '6. 教えて育てる', 293 | mwb_song_middle: 155, 294 | mwb_lc_count: 1, 295 | mwb_lc_part1: '神の約束が果たされる時を楽しみにする', 296 | mwb_lc_part1_time: 15, 297 | mwb_lc_part1_title: '7. 神の約束が果たされる時を楽しみにする', 298 | mwb_lc_part1_content: '討議。', 299 | mwb_lc_cbs: '20章8-12節,161ページ囲み', 300 | mwb_lc_cbs_title: '8. 会衆の聖書研究', 301 | mwb_song_conclude: 58, 302 | }, 303 | ]; 304 | -------------------------------------------------------------------------------- /test/fixtures/mwb_KO_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | mwb_week_date: '2024/11/04', 4 | mwb_week_date_locale: '11월 4-10일', 5 | mwb_weekly_bible_reading: '시편 105편', 6 | mwb_song_first: 3, 7 | mwb_tgw_talk: '‘그분은 자신의 계약을 영원히 기억하신다’', 8 | mwb_tgw_talk_title: '1. ‘그분은 자신의 계약을 영원히 기억하신다’', 9 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 10 | mwb_tgw_bread: '시 105:24-45 (「읽가」 5과)', 11 | mwb_tgw_bread_title: '3. 성경 낭독', 12 | mwb_ayf_count: 4, 13 | mwb_ayf_part1: '호별 방문. 바쁜 집주인을 만난다. (「랑제」 2과 요점 5)', 14 | mwb_ayf_part1_time: 1, 15 | mwb_ayf_part1_type: '대화 시작하기', 16 | mwb_ayf_part1_title: '4. 대화 시작하기', 17 | mwb_ayf_part2: '호별 방문. 상대방이 논쟁을 하려고 할 때 친절한 방식으로 대화를 끝낸다. (「랑제」 4과 요점 5)', 18 | mwb_ayf_part2_time: 2, 19 | mwb_ayf_part2_type: '대화 시작하기', 20 | mwb_ayf_part2_title: '5. 대화 시작하기', 21 | mwb_ayf_part3: '호별 방문. 지난번에 집주인이 관심을 나타냈던 주제를 다루는 잡지를 전한다. (「랑제」 8과 요점 3)', 22 | mwb_ayf_part3_time: 4, 23 | mwb_ayf_part3_type: '관심이 자라도록 돕기', 24 | mwb_ayf_part3_title: '6. 관심이 자라도록 돕기', 25 | mwb_ayf_part4: '비공식 증거. 상대방에게 「JW 라이브러리」 앱을 소개하고 다운로드할 수 있게 도와준다. (「랑제」 9과 요점 5)', 26 | mwb_ayf_part4_time: 4, 27 | mwb_ayf_part4_type: '관심이 자라도록 돕기', 28 | mwb_ayf_part4_title: '7. 관심이 자라도록 돕기', 29 | mwb_song_middle: 84, 30 | mwb_lc_count: 1, 31 | mwb_lc_part1: '그리스도에 대한 사랑을 표현하는 방법', 32 | mwb_lc_part1_time: 15, 33 | mwb_lc_part1_title: '8. 그리스도에 대한 사랑을 표현하는 방법', 34 | mwb_lc_part1_content: '토의.', 35 | mwb_lc_cbs: '「철」 17장 13-19항', 36 | mwb_lc_cbs_title: '9. 회중 성서 연구', 37 | mwb_song_conclude: 97 38 | }, 39 | { 40 | mwb_week_date: '2024/11/11', 41 | mwb_week_date_locale: '11월 11-17일', 42 | mwb_weekly_bible_reading: '시편 106편', 43 | mwb_song_first: 36, 44 | mwb_tgw_talk: '“그들이 잊었구나, 자기들의 구원자이신 하느님을”', 45 | mwb_tgw_talk_title: '1. “그들이 잊었구나, 자기들의 구원자이신 하느님을”', 46 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 47 | mwb_tgw_bread: '시 106:21-48 (「읽가」 10과)', 48 | mwb_tgw_bread_title: '3. 성경 낭독', 49 | mwb_ayf_count: 2, 50 | mwb_ayf_part1: '토의. 동영상을 보여 주고 「랑제」 11과 요점 1-2를 토의한다.', 51 | mwb_ayf_part1_time: 7, 52 | mwb_ayf_part1_type: '간단명료하게 가르치기—예수의 본', 53 | mwb_ayf_part1_title: '4. 간단명료하게 가르치기—예수의 본', 54 | mwb_ayf_part2: '「랑제」 11과 요점 3-5 및 “다음 내용도 살펴보십시오”에 근거한 토의.', 55 | mwb_ayf_part2_time: 8, 56 | mwb_ayf_part2_type: '간단명료하게 가르치기—예수를 본받으십시오', 57 | mwb_ayf_part2_title: '5. 간단명료하게 가르치기—예수를 본받으십시오', 58 | mwb_song_middle: 78, 59 | mwb_lc_count: 1, 60 | mwb_lc_part1: '회중의 필요', 61 | mwb_lc_part1_time: 15, 62 | mwb_lc_part1_title: '6. 회중의 필요', 63 | mwb_lc_cbs: '「철」 18장 1-5항 및 142, 144면 네모', 64 | mwb_lc_cbs_title: '7. 회중 성서 연구', 65 | mwb_song_conclude: 77 66 | }, 67 | { 68 | mwb_week_date: '2024/11/18', 69 | mwb_week_date_locale: '11월 18-24일', 70 | mwb_weekly_bible_reading: '시편 107-108편', 71 | mwb_song_first: 7, 72 | mwb_tgw_talk: '“여호와께 감사드려라. 그분은 선하시다”', 73 | mwb_tgw_talk_title: '1. “여호와께 감사드려라. 그분은 선하시다”', 74 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 75 | mwb_tgw_bread: '시 107:1-28 (「읽가」 5과)', 76 | mwb_tgw_bread_title: '3. 성경 낭독', 77 | mwb_ayf_count: 3, 78 | mwb_ayf_part1: '비공식 증거. (「랑제」 1과 요점 4)', 79 | mwb_ayf_part1_time: 3, 80 | mwb_ayf_part1_type: '대화 시작하기', 81 | mwb_ayf_part1_title: '4. 대화 시작하기', 82 | mwb_ayf_part2: '비공식 증거. 성경 공부 과정에 대해 설명하고, 성경 공부 소개 카드를 전한다. (「랑제」 9과 요점 3)', 83 | mwb_ayf_part2_time: 4, 84 | mwb_ayf_part2_type: '관심이 자라도록 돕기', 85 | mwb_ayf_part2_title: '5. 관심이 자라도록 돕기', 86 | mwb_ayf_part3: '「웹청묻」 기사 90—주제: 어떻게 하면 부정적인 생각을 안 할 수 있을까? (「읽가」 14과)', 87 | mwb_ayf_part3_time: 5, 88 | mwb_ayf_part3_type: '연설', 89 | mwb_ayf_part3_title: '6. 연설', 90 | mwb_song_middle: 46, 91 | mwb_lc_count: 1, 92 | mwb_lc_part1: '우리는 노래로 여호와께 감사를 표현합니다', 93 | mwb_lc_part1_time: 15, 94 | mwb_lc_part1_title: '7. 우리는 노래로 여호와께 감사를 표현합니다', 95 | mwb_lc_part1_content: '토의.', 96 | mwb_lc_cbs: '「철」 18장 6-15항', 97 | mwb_lc_cbs_title: '8. 회중 성서 연구', 98 | mwb_song_conclude: 73 99 | }, 100 | { 101 | mwb_week_date: '2024/11/25', 102 | mwb_week_date_locale: '11월 25일–12월 1일', 103 | mwb_weekly_bible_reading: '시편 109-112편', 104 | mwb_song_first: 14, 105 | mwb_tgw_talk: '왕이신 예수를 지지하십시오!', 106 | mwb_tgw_talk_title: '1. 왕이신 예수를 지지하십시오!', 107 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 108 | mwb_tgw_bread: '시 109:1-26 (「읽가」 2과)', 109 | mwb_tgw_bread_title: '3. 성경 낭독', 110 | mwb_ayf_count: 3, 111 | mwb_ayf_part1: '호별 방문. 전도지를 사용해 대화를 시작한다. (「랑제」 4과 요점 3)', 112 | mwb_ayf_part1_time: 2, 113 | mwb_ayf_part1_type: '대화 시작하기', 114 | mwb_ayf_part1_title: '4. 대화 시작하기', 115 | mwb_ayf_part2: '실연. 「웹증질」 기사 23—주제: 여호와의 증인이 전쟁에 참여하지 않는 이유는 무엇입니까? (「랑제」 4과 요점 4)', 116 | mwb_ayf_part2_time: 5, 117 | mwb_ayf_part2_type: '우리의 신앙 설명하기', 118 | mwb_ayf_part2_title: '5. 우리의 신앙 설명하기', 119 | mwb_ayf_part3: '「행누」 15과 요점 6 및 “사람들이 하는 말” (「랑제」 11과 요점 3)', 120 | mwb_ayf_part3_time: 5, 121 | mwb_ayf_part3_type: '제자가 되도록 돕기', 122 | mwb_ayf_part3_title: '6. 제자가 되도록 돕기', 123 | mwb_song_middle: 72, 124 | mwb_lc_count: 1, 125 | mwb_lc_part1: '어떻게 왕국을 충성스럽게 지지할 수 있습니까?', 126 | mwb_lc_part1_time: 15, 127 | mwb_lc_part1_title: '7. 어떻게 왕국을 충성스럽게 지지할 수 있습니까?', 128 | mwb_lc_part1_content: '토의.', 129 | mwb_lc_cbs: '「철」 18장 16-24항', 130 | mwb_lc_cbs_title: '8. 회중 성서 연구', 131 | mwb_song_conclude: 75 132 | }, 133 | { 134 | mwb_week_date: '2024/12/02', 135 | mwb_week_date_locale: '12월 2-8일', 136 | mwb_weekly_bible_reading: '시편 113-118편', 137 | mwb_song_first: 127, 138 | mwb_tgw_talk: '여호와께 무엇으로 보답할 수 있겠습니까?', 139 | mwb_tgw_talk_title: '1. 여호와께 무엇으로 보답할 수 있겠습니까?', 140 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 141 | mwb_tgw_bread: '시 116:1–117:2 (「읽가」 2과)', 142 | mwb_tgw_bread_title: '3. 성경 낭독', 143 | mwb_ayf_count: 2, 144 | mwb_ayf_part1: '토의. 동영상을 보여 주고 「랑제」 12과 요점 1-2를 토의한다.', 145 | mwb_ayf_part1_time: 7, 146 | mwb_ayf_part1_type: '용기—예수의 본', 147 | mwb_ayf_part1_title: '4. 용기—예수의 본', 148 | mwb_ayf_part2: '「랑제」 12과 요점 3-5 및 “다음 내용도 살펴보십시오”에 근거한 토의.', 149 | mwb_ayf_part2_time: 8, 150 | mwb_ayf_part2_type: '용기—예수를 본받으십시오', 151 | mwb_ayf_part2_title: '5. 용기—예수를 본받으십시오', 152 | mwb_song_middle: 60, 153 | mwb_lc_count: 1, 154 | mwb_lc_part1: '회중의 필요', 155 | mwb_lc_part1_time: 15, 156 | mwb_lc_part1_title: '6. 회중의 필요', 157 | mwb_lc_cbs: '「철」 19장 1-5항 및 149-150면 네모', 158 | mwb_lc_cbs_title: '7. 회중 성서 연구', 159 | mwb_song_conclude: 29 160 | }, 161 | { 162 | mwb_week_date: '2024/12/09', 163 | mwb_week_date_locale: '12월 9-15일', 164 | mwb_weekly_bible_reading: '시편 119:1-56', 165 | mwb_song_first: 124, 166 | mwb_tgw_talk: '“젊은이가 어떻게 자기의 행로를 깨끗이 지킬 수 있습니까?”', 167 | mwb_tgw_talk_title: '1. “젊은이가 어떻게 자기의 행로를 깨끗이 지킬 수 있습니까?”', 168 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 169 | mwb_tgw_bread: '시 119:1-32 (「읽가」 5과)', 170 | mwb_tgw_bread_title: '3. 성경 낭독', 171 | mwb_ayf_count: 3, 172 | mwb_ayf_part1: '비공식 증거. 호별 방문 중에 길을 걸어가다가 만난 사람과 대화를 시작한다. (「랑제」 1과 요점 4)', 173 | mwb_ayf_part1_time: 3, 174 | mwb_ayf_part1_type: '대화 시작하기', 175 | mwb_ayf_part1_title: '4. 대화 시작하기', 176 | mwb_ayf_part2: '호별 방문. 지난번 방문에서 최근에 사랑하는 사람과 사별했다고 말한 사람을 다시 방문한다. (「랑제」 9과 요점 3)', 177 | mwb_ayf_part2_time: 4, 178 | mwb_ayf_part2_type: '관심이 자라도록 돕기', 179 | mwb_ayf_part2_title: '5. 관심이 자라도록 돕기', 180 | mwb_ayf_part3: '「웹청묻」 기사 83—주제: 어떻게 하면 유혹을 물리칠 수 있을까? (「읽가」 20과)', 181 | mwb_ayf_part3_time: 5, 182 | mwb_ayf_part3_type: '연설', 183 | mwb_ayf_part3_title: '6. 연설', 184 | mwb_song_middle: 40, 185 | mwb_lc_count: 2, 186 | mwb_lc_part1: '「우리의 조직이 이루고 있는 일들」 12월', 187 | mwb_lc_part1_time: 10, 188 | mwb_lc_part1_title: '7. 「우리의 조직이 이루고 있는 일들」 12월', 189 | mwb_lc_part1_content: '동영상을 보여 준다.', 190 | mwb_lc_part2: '회중의 필요', 191 | mwb_lc_part2_time: 5, 192 | mwb_lc_part2_title: '8. 회중의 필요', 193 | mwb_lc_cbs: '「철」 19장 6-13항', 194 | mwb_lc_cbs_title: '9. 회중 성서 연구', 195 | mwb_song_conclude: 21 196 | }, 197 | { 198 | mwb_week_date: '2024/12/16', 199 | mwb_week_date_locale: '12월 16-22일', 200 | mwb_weekly_bible_reading: '시편 119:57-120', 201 | mwb_song_first: 129, 202 | mwb_tgw_talk: '고난을 인내하는 방법', 203 | mwb_tgw_talk_title: '1. 고난을 인내하는 방법', 204 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 205 | mwb_tgw_bread: '시 119:57-80 (「읽가」 12과)', 206 | mwb_tgw_bread_title: '3. 성경 낭독', 207 | mwb_ayf_count: 3, 208 | mwb_ayf_part1: '호별 방문. 집주인에게 우리의 웹사이트를 보여 주고, jw.org 카드를 전한다. (「랑제」 2과 요점 5)', 209 | mwb_ayf_part1_time: 3, 210 | mwb_ayf_part1_type: '대화 시작하기', 211 | mwb_ayf_part1_title: '4. 대화 시작하기', 212 | mwb_ayf_part2: '비공식 증거. 공개 강연에 초대한다. 「왕국회관은 어떤 곳입니까?」 동영상을 사용해 대화한다. (「랑제」 8과 요점 3)', 213 | mwb_ayf_part2_time: 4, 214 | mwb_ayf_part2_type: '관심이 자라도록 돕기', 215 | mwb_ayf_part2_title: '5. 관심이 자라도록 돕기', 216 | mwb_ayf_part3: '실연. 「웹성대」 기사 157—주제: 성경은 자연재해에 대해 무엇을 알려 줍니까? (「랑제」 3과 요점 3)', 217 | mwb_ayf_part3_time: 5, 218 | mwb_ayf_part3_type: '우리의 신앙 설명하기', 219 | mwb_ayf_part3_title: '6. 우리의 신앙 설명하기', 220 | mwb_song_middle: 128, 221 | mwb_lc_count: 1, 222 | mwb_lc_part1: '여호와께서는 우리가 인내하도록 도와주십니다', 223 | mwb_lc_part1_time: 15, 224 | mwb_lc_part1_title: '7. 여호와께서는 우리가 인내하도록 도와주십니다', 225 | mwb_lc_part1_content: '토의.', 226 | mwb_lc_cbs: '「철」 19장 14-20항 및 152면 네모', 227 | mwb_lc_cbs_title: '8. 회중 성서 연구', 228 | mwb_song_conclude: 32 229 | }, 230 | { 231 | mwb_week_date: '2024/12/23', 232 | mwb_week_date_locale: '12월 23-29일', 233 | mwb_weekly_bible_reading: '시편 119:121-176', 234 | mwb_song_first: 31, 235 | mwb_tgw_talk: '불필요한 고통을 자초하지 않는 방법', 236 | mwb_tgw_talk_title: '1. 불필요한 고통을 자초하지 않는 방법', 237 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 238 | mwb_tgw_bread: '시 119:121-152 (「읽가」 2과)', 239 | mwb_tgw_bread_title: '3. 성경 낭독', 240 | mwb_ayf_count: 3, 241 | mwb_ayf_part1: '호별 방문. (「랑제」 1과 요점 5)', 242 | mwb_ayf_part1_time: 3, 243 | mwb_ayf_part1_type: '대화 시작하기', 244 | mwb_ayf_part1_title: '4. 대화 시작하기', 245 | mwb_ayf_part2: '호별 방문. 상대방이 관심을 가질 만한 자료를 jw.org에서 어떻게 찾을 수 있는지 보여 준다. (「랑제」 8과 요점 3)', 246 | mwb_ayf_part2_time: 4, 247 | mwb_ayf_part2_type: '관심이 자라도록 돕기', 248 | mwb_ayf_part2_title: '5. 관심이 자라도록 돕기', 249 | mwb_ayf_part3: '회중 집회에 정기적으로 참석하지 않는 성서 연구생과 토의한다. (「랑제」 12과 요점 4)', 250 | mwb_ayf_part3_time: 5, 251 | mwb_ayf_part3_type: '제자가 되도록 돕기', 252 | mwb_ayf_part3_title: '6. 제자가 되도록 돕기', 253 | mwb_song_middle: 121, 254 | mwb_lc_count: 1, 255 | mwb_lc_part1: '돈 때문에 불필요한 고통을 겪지 마십시오', 256 | mwb_lc_part1_time: 15, 257 | mwb_lc_part1_title: '7. 돈 때문에 불필요한 고통을 겪지 마십시오', 258 | mwb_lc_part1_content: '토의.', 259 | mwb_lc_cbs: '「철」 20장 1-7항 및 7부 소개말', 260 | mwb_lc_cbs_title: '8. 회중 성서 연구', 261 | mwb_song_conclude: 101 262 | }, 263 | { 264 | mwb_week_date: '2024/12/30', 265 | mwb_week_date_locale: '2024년 12월 30일–2025년 1월 5일', 266 | mwb_weekly_bible_reading: '시편 120-126편', 267 | mwb_song_first: 144, 268 | mwb_tgw_talk: '그들은 눈물로 씨를 뿌렸지만 기뻐하며 거두어들였습니다', 269 | mwb_tgw_talk_title: '1. 그들은 눈물로 씨를 뿌렸지만 기뻐하며 거두어들였습니다', 270 | mwb_tgw_gems_title: '2. 영적 보물 찾기', 271 | mwb_tgw_bread: '시 124:1–126:6 (「읽가」 5과)', 272 | mwb_tgw_bread_title: '3. 성경 낭독', 273 | mwb_ayf_count: 3, 274 | mwb_ayf_part1: '공개 증거. (「랑제」 3과 요점 5)', 275 | mwb_ayf_part1_time: 3, 276 | mwb_ayf_part1_type: '대화 시작하기', 277 | mwb_ayf_part1_title: '4. 대화 시작하기', 278 | mwb_ayf_part2: '호별 방문. 지난번 방문에서 성경에 대해 의심을 표현한 사람을 다시 방문한다. (「랑제」 9과 요점 5)', 279 | mwb_ayf_part2_time: 4, 280 | mwb_ayf_part2_type: '관심이 자라도록 돕기', 281 | mwb_ayf_part2_title: '5. 관심이 자라도록 돕기', 282 | mwb_ayf_part3: '「행누」 16과 소개말 및 요점 1-3 (「랑제」 11과 요점 3)', 283 | mwb_ayf_part3_time: 5, 284 | mwb_ayf_part3_type: '제자가 되도록 돕기', 285 | mwb_ayf_part3_title: '6. 제자가 되도록 돕기', 286 | mwb_song_middle: 155, 287 | mwb_lc_count: 1, 288 | mwb_lc_part1: '하느님의 약속을 통해 기쁨을 얻으십시오', 289 | mwb_lc_part1_time: 15, 290 | mwb_lc_part1_title: '7. 하느님의 약속을 통해 기쁨을 얻으십시오', 291 | mwb_lc_part1_content: '토의.', 292 | mwb_lc_cbs: '「철」 20장 8-12항 및 161면 네모', 293 | mwb_lc_cbs_title: '8. 회중 성서 연구', 294 | mwb_song_conclude: 58 295 | } 296 | ] -------------------------------------------------------------------------------- /test/fixtures/w_CHS_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '研究班课文44:2025年1月6-12日', 5 | w_study_title: '遇到不公平的事该怎么办?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '研究班课文45:2025年1月13-19日', 12 | w_study_title: '三位忠仆留给我们的话', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '研究班课文46:2025年1月20-26日', 19 | w_study_title: '弟兄们,你有没有竭力符合资格做助理仆人?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '研究班课文47:2025年1月27日~2月2日', 26 | w_study_title: '弟兄们,你有没有竭力符合资格做长老?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_CH_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '研究班課文44:2025年1月6-12日', 5 | w_study_title: '遇到不公平的事該怎麼辦?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '研究班課文45:2025年1月13-19日', 12 | w_study_title: '三位忠僕留給我們的話', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '研究班課文46:2025年1月20-26日', 19 | w_study_title: '弟兄們,你有沒有竭力符合資格做助理僕人?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '研究班課文47:2025年1月27日~2月2日', 26 | w_study_title: '弟兄們,你有沒有竭力符合資格做長老?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_CR_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Atik etid no 44: Semèn 6-12 janvye 2025', 5 | w_study_title: 'Fason n ka andire enjistis yo fè nou', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Atik etid no 45: Semèn 13-19 janvye 2025', 12 | w_study_title: 'Sa n ka aprann nan dènye pawòl yon seri mesye ki te fidèl te di', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Atik etid no 46: Semèn 20-26 janvye 2025', 19 | w_study_title: 'Frè n yo, èske n ap chèche vin sèvitè ministeryèl?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Atik etid no 47: Semèn 27 janvye 2025–2 fevriye 2025', 26 | w_study_title: 'Frè n yo, èske n ap chèche vin ansyen?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/fixtures/w_D_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: 'Studieartikel 44, 6.-12. januar 2025', 4 | w_study_title: 'Hvordan du kan tackle uretfærdighed', 5 | w_study_opening_song: 33, 6 | w_study_concluding_song: 38, 7 | }, 8 | { 9 | w_study_date: 'Studieartikel 45, 13.-19. januar 2025', 10 | w_study_title: 'Lær af trofaste mænds afskedsord', 11 | w_study_opening_song: 138, 12 | w_study_concluding_song: 129, 13 | }, 14 | { 15 | w_study_date: 'Studieartikel 46, 20.-26. januar 2025', 16 | w_study_title: 'Arbejder du på at blive menighedstjener?', 17 | w_study_opening_song: 49, 18 | w_study_concluding_song: 17, 19 | }, 20 | { 21 | w_study_date: 'Studieartikel 47, 27. januar 2025 – 2. februar 2025', 22 | w_study_title: 'Arbejder du på at blive ældste?', 23 | w_study_opening_song: 103, 24 | w_study_concluding_song: 101, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /test/fixtures/w_ELI_202407.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2024/09/09', 4 | w_study_date_locale: 'Study Article 27: September 9-15, 2024', 5 | w_study_title: 'Be Brave Like Zadok', 6 | w_study_opening_song: 73, 7 | w_study_concluding_song: 126 8 | }, 9 | { 10 | w_study_date: '2024/09/16', 11 | w_study_date_locale: 'Study Article 28: September 16-22, 2024', 12 | w_study_title: 'You Know the Difference Between Truth and Lie?', 13 | w_study_opening_song: 123, 14 | w_study_concluding_song: 122 15 | }, 16 | { 17 | w_study_date: '2024/09/23', 18 | w_study_date_locale: 'Study Article 29: September 23-29, 2024', 19 | w_study_title: 'Continue to Fight Against Temptation ', 20 | w_study_opening_song: 121, 21 | w_study_concluding_song: 47 22 | }, 23 | { 24 | w_study_date: '2024/09/30', 25 | w_study_date_locale: 'Study Article 30: September 30, 2024–October 6, 2024', 26 | w_study_title: 'We Can Learn Important Lessons From the King Them in Israel', 27 | w_study_opening_song: 36, 28 | w_study_concluding_song: 45 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_E_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Study Article 44: January 6-12, 2025', 5 | w_study_title: 'How to Cope With Injustice', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Study Article 45: January 13-19, 2025', 12 | w_study_title: 'Learn From the Parting Words of Faithful Men', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Study Article 46: January 20-26, 2025', 19 | w_study_title: 'Brothers​—Are You Reaching Out to Be a Ministerial Servant?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Study Article 47: January 27, 2025–February 2, 2025', 26 | w_study_title: 'Brothers​—Are You Reaching Out to Serve as an Elder?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_F_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Article d’étude no 44 : 6-12 janvier 2025', 5 | w_study_title: 'Comment réagir à l’injustice', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Article d’étude no 45 : 13-19 janvier 2025', 12 | w_study_title: 'Tirons leçon des dernières paroles d’hommes de foi', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Article d’étude no 46 : 20-26 janvier 2025', 19 | w_study_title: 'Frères, vous qualifiez-vous pour devenir assistants ?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Article d’étude no 47 : 27 janvier 2025 – 2 février 2025', 26 | w_study_title: 'Frères, vous qualifiez-vous pour devenir anciens ?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_IL_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Maadal nga Artikulo 44: Enero 6-12, 2025', 5 | w_study_title: 'No Kasano nga Ibturan ti Kinaawan Hustisia', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Maadal nga Artikulo 45: Enero 13-19, 2025', 12 | w_study_title: 'Ti Masursurotayo iti Pammakada a Sasao Dagiti Matalek a Lallaki', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Maadal nga Artikulo 46: Enero 20-26, 2025', 19 | w_study_title: 'Kakabsat a Lallaki—Kalatyo Kadi ti Agbalin a Ministerial nga Adipen?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Maadal nga Artikulo 47: Enero 27, 2025–Pebrero 2, 2025', 26 | w_study_title: 'Kakabsat a Lallaki—Kalatyo Kadi ti Agserbi kas Panglakayen?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_I_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Articolo di studio 44 (6-12 gennaio 2025)', 5 | w_study_title: 'Come far fronte alle ingiustizie', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Articolo di studio 45 (13-19 gennaio 2025)', 12 | w_study_title: 'Impariamo dalle ultime parole di uomini fedeli', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Articolo di studio 46 (20-26 gennaio 2025)', 19 | w_study_title: 'Fratelli, aspirate a essere servitori di ministero?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Articolo di studio 47 (27 gennaio 2025 – 2 febbraio 2025)', 26 | w_study_title: 'Fratelli, aspirate a servire come anziani?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_J_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '研究記事44: 2025年1月6-12日', 5 | w_study_title: '不当な扱いを受けた時に心に留めておきたいこと', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '研究記事45: 2025年1月13-19日', 12 | w_study_title: '神に仕えた人たちが残した言葉から学ぶ', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '研究記事46: 2025年1月20-26日', 19 | w_study_title: '援助奉仕者として奉仕することを目標にできますか', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '研究記事47: 2025年1月27日-2月2日', 26 | w_study_title: '長老として奉仕することを目標にできますか', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_KO_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '연구 기사 44: 2025년 1월 6-12일', 5 | w_study_title: '부당한 일을 겪을 때 어떻게 해야 합니까?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '연구 기사 45: 2025년 1월 13-19일', 12 | w_study_title: '충실한 사람들이 마지막으로 남긴 조언을 통해 배우십시오', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '연구 기사 46: 2025년 1월 20-26일', 19 | w_study_title: '형제 여러분, 봉사의 종으로 섬기기 위해 힘써 노력하고 있습니까?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '연구 기사 47: 2025년 1월 27일–2025년 2월 2일', 26 | w_study_title: '형제 여러분, 장로로 섬기기 위해 힘써 노력하고 있습니까?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_K_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Стаття для вивчення 44 (6—12 січня 2025)', 5 | w_study_title: 'Не дайте несправедливості вас зламати', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Стаття для вивчення 45 (13—19 січня 2025)', 12 | w_study_title: 'Прощальні слова вірних чоловіків давнини', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Стаття для вивчення 46 (20—26 січня 2025)', 19 | w_study_title: 'Брати, чи ви прагнете бути служителями збору?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Стаття для вивчення 47 (27 січня 2025 — 2 лютого 2025)', 26 | w_study_title: 'Брати, чи ви прагнете служити старійшинами?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_MG_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Lahatsoratra Fianarana 44: 6-12 Janoary 2025', 5 | w_study_title: 'Ahoana no Iaretana ny Tsy Rariny?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Lahatsoratra Fianarana 45: 13-19 Janoary 2025', 12 | w_study_title: 'Ny Hafatrafatr’ireo Lehilahy Tsy Nivadika', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Lahatsoratra Fianarana 46: 20-26 Janoary 2025', 19 | w_study_title: 'Ry Rahalahy, Miezaka ho Lasa Mpikarakara Fiangonana ve Ianareo?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Lahatsoratra Fianarana 47: 27 Janoary 2025–2 Febroary 2025', 26 | w_study_title: 'Ry Rahalahy, Miezaka ho Lasa Anti-panahy ve Ianareo?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_ML_202501.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/03/03', 4 | w_study_date_locale: 'Rencana Pembelajaran 1: 3-9 Mac 2025', 5 | w_study_title: 'Muliakanlah Yehuwa', 6 | w_study_opening_song: 2, 7 | w_study_concluding_song: 159 8 | }, 9 | { 10 | w_study_date: '2025/03/10', 11 | w_study_date_locale: 'Rencana Pembelajaran 2: 10-16 Mac 2025', 12 | w_study_title: 'Para Suami, Hormatilah Isteri Anda', 13 | w_study_opening_song: 132, 14 | w_study_concluding_song: 131 15 | }, 16 | { 17 | w_study_date: '2025/03/17', 18 | w_study_date_locale: 'Rencana Pembelajaran 3: 17-23 Mac 2025', 19 | w_study_title: 'Buatlah Keputusan yang Menyenangkan Hati Yehuwa', 20 | w_study_opening_song: 35, 21 | w_study_concluding_song: 28 22 | }, 23 | { 24 | w_study_date: '2025/03/24', 25 | w_study_date_locale: 'Rencana Pembelajaran 4: 24-30 Mac 2025', 26 | w_study_title: 'Apa yang Kita Belajar daripada Korban Tebusan', 27 | w_study_opening_song: 18, 28 | w_study_concluding_song: 107 29 | }, 30 | { 31 | w_study_date: '2025/03/31', 32 | w_study_date_locale: 'Rencana Pembelajaran 5: 31 Mac 2025–6 April 2025', 33 | w_study_title: 'Manfaat yang Kita Nikmati kerana Yehuwa Mengasihi Kita', 34 | w_study_opening_song: 108, 35 | w_study_concluding_song: 154 36 | } 37 | ] -------------------------------------------------------------------------------- /test/fixtures/w_M_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Articolul de studiu 44: 6-12 ianuarie 2025', 5 | w_study_title: 'Cum poți persevera în pofida nedreptății?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Articolul de studiu 45: 13-19 ianuarie 2025', 12 | w_study_title: 'Să învățăm din ultimele cuvinte ale slujitorilor fideli', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Articolul de studiu 46: 20-26 ianuarie 2025', 19 | w_study_title: 'Dragi frați, v-ați stabilit obiectivul de a deveni slujitori auxiliari?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Articolul de studiu 47: 27 ianuarie 2025 – 2 februarie 2025', 26 | w_study_title: 'Dragi frați, v-ați stabilit obiectivul de a deveni bătrâni de congregație?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_O_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Studieartikel 44: 6-12 januari 2025', 5 | w_study_title: 'Hoe je kunt omgaan met onrecht', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Studieartikel 45: 13-19 januari 2025', 12 | w_study_title: 'De leerzame afscheidswoorden van drie trouwe mannen', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Studieartikel 46: 20-26 januari 2025', 19 | w_study_title: 'Streef jij ernaar dienaar te worden?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Studieartikel 47: 27 januari 2025–2 februari 2025', 26 | w_study_title: 'Streef jij ernaar als ouderling te dienen?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/fixtures/w_PGW_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Study Article 44: January 6-12, 2025', 5 | w_study_title: 'Wetin We Fit Do When People No Treat Us Well?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Study Article 45: January 13-19, 2025', 12 | w_study_title: 'Wetin We Fit Learn From the Words Wey Some Servants of Jehovah Talk Before Them Die?', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Study Article 46: January 20-26, 2025', 19 | w_study_title: 'Brothers, Una De Work Hard to Fit Become Ministerial Servants?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Study Article 47: January 27, 2025–February 2, 2025', 26 | w_study_title: 'Brothers, Una De Work Hard to Fit Become Elders?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/fixtures/w_SV_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '44. preučevalni članek: od 6. do 12. januarja 2025', 5 | w_study_title: 'Kako lahko ostaneš zvest Jehovu kljub krivicam', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '45. preučevalni članek: od 13. do 19. januarja 2025', 12 | w_study_title: 'Kaj se lahko naučimo iz zadnjih besed zvestih Božjih služabnikov?', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '46. preučevalni članek: od 20. do 26. januarja 2025', 19 | w_study_title: 'Bratje, ali si prizadevate, da bi služili kot strežni služabniki?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '47. preučevalni članek: od 27. januarja 2025 do 2. februarja 2025', 26 | w_study_title: 'Bratje, ali si prizadevate, da bi služili kot starešine?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/fixtures/w_SW_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Makala ya 44: Januari 6-12, 2025', 5 | w_study_title: 'Jinsi ya Kukabiliana na Ukosefu wa Haki', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Makala ya 45: Januari 13-19, 2025', 12 | w_study_title: 'Jifunze Kutokana na Maneno ya Mwisho ya Wanaume Waaminifu', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Makala ya 46: Januari 20-26, 2025', 19 | w_study_title: 'Akina Ndugu—Je, Mnajitahidi Kustahili Kuwa Watumishi wa Huduma?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Makala ya 47: Januari 27, 2025–Februari 2, 2025', 26 | w_study_title: 'Akina Ndugu—Je, Mnajitahidi Kustahili Kutumikia Mkiwa Wazee wa Kutaniko?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/fixtures/w_S_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Artículo de estudio 44 (del 6 al 12 de enero de 2025)', 5 | w_study_title: 'Cómo reaccionar ante las injusticias', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Artículo de estudio 45 (del 13 al 19 de enero de 2025)', 12 | w_study_title: 'Lecciones de los consejos de despedida de hombres fieles', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Artículo de estudio 46 (del 20 al 26 de enero de 2025)', 19 | w_study_title: '¿Tiene la meta de ser siervo ministerial?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Artículo de estudio 47 (del 27 de enero de 2025 al 2 de febrero de 2025)', 26 | w_study_title: '¿Tiene la meta de ser anciano?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_TG_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Araling Artikulo 44: Enero 6-12, 2025', 5 | w_study_title: 'Kung Paano Haharapin ang Kawalang-Katarungan', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Araling Artikulo 45: Enero 13-19, 2025', 12 | w_study_title: 'Matuto Mula sa mga Huling Habilin ng Tapat na mga Lalaki', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Araling Artikulo 46: Enero 20-26, 2025', 19 | w_study_title: 'Mga Brother—Sinisikap Ba Ninyong Maging Ministeryal na Lingkod?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Araling Artikulo 47: Enero 27, 2025–Pebrero 2, 2025', 26 | w_study_title: 'Mga Brother—Sinisikap Ba Ninyong Maging Elder?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_TK_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '44. İnceleme Makalesi: 6-12 Ocak 2025', 5 | w_study_title: 'Adaletsizlikle Nasıl Başa Çıkabiliriz?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '45. İnceleme Makalesi: 13-19 Ocak 2025', 12 | w_study_title: 'İmanlı Adamların Son Sözlerinden Öğrenebileceklerimiz', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '46. İnceleme Makalesi: 20-26 Ocak 2025', 19 | w_study_title: 'Biraderler, Hizmet Görevlisi Olabilir misiniz?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '47. İnceleme Makalesi: 27 Ocak 2025–2 Şubat 2025', 26 | w_study_title: 'Biraderler, İhtiyar Olarak Hizmet Edebilir misiniz?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_TND_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Lahatsoratse Fianaragne 44: 6-12 Janvie 2025', 5 | w_study_title: 'Ino ty Hagnampe Azo Hiatreke Tsy Rare’e?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Lahatsoratse Fianaragne 45: 13-19 Janvie 2025', 12 | w_study_title: 'Fianaragne Boake amy ty Hafatse Fara’e Nirehafe i Lahilahy Tsy Nivalike Rey', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Lahatsoratse Fianaragne 46: 20-26 Janvie 2025', 19 | w_study_title: 'Ry Rahalahy, Mikezake ty ho Mpagnampe amy ty Fiangonagne vao Nareo?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Lahatsoratse Fianaragne 47: 27 Janvie 2025–2 Fevrie 2025', 26 | w_study_title: 'Ry Rahalahy, Mikezake ty ho Androanavi-pagnahy vao Nareo?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /test/fixtures/w_TNK_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Lahatsoratra Fianaran̈a 44: 6-12 Zanvie 2025', 5 | w_study_title: 'Akôry Hahavitanao Hiaritry Raha Tsy Rariny?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Lahatsoratra Fianaran̈a 45: 13-19 Zanvie 2025', 12 | w_study_title: 'Mianara Baka amy Volan̈a Faramparany Nataondrô Lelahy Tsy Mivadiky aby Io', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Lahatsoratra Fianaran̈a 46: 20-26 Zanvie 2025', 19 | w_study_title: 'Anarô Rahahaly, Mifen̈y ho Mpikarakara Fiangonan̈a ma Anarô?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Lahatsoratra Fianaran̈a 47: 27 Zanvie 2025–2 Fevrie 2025', 26 | w_study_title: 'Anarô Rahalahy, Mifen̈y Hanjary Anti-pan̈ahy ma Anarô?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_TPO_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Artigo 44 | 6 a 12 de janeiro de 2025', 5 | w_study_title: 'Como lidar com injustiças', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Artigo 45 | 13 a 19 de janeiro de 2025', 12 | w_study_title: 'Aprenda com as últimas palavras de homens fiéis do passado', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Artigo 46 | 20 a 26 de janeiro de 2025', 19 | w_study_title: 'Está a esforçar-se para ser servo ministerial?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Artigo 47 | 27 de janeiro de 2025 a 2 de fevereiro de 2025', 26 | w_study_title: 'Está a esforçar-se para ser ancião?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_U_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Статья для изучения 44 (6—12 января 2025)', 5 | w_study_title: 'Как реагировать на несправедливость', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Статья для изучения 45 (13—19 января 2025)', 12 | w_study_title: 'Чему нас учат последние слова верных служителей Бога', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Статья для изучения 46 (20—26 января 2025)', 19 | w_study_title: 'Братья, есть ли у вас цель стать помощниками собрания?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Статья для изучения 47 (27 января 2025 — 2 февраля 2025)', 26 | w_study_title: 'Братья, есть ли у вас цель стать старейшинами?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_VT_202501.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/03/03', 4 | w_study_date_locale: 'Bài học 1: Ngày 3-9 tháng 3 năm 2025', 5 | w_study_title: 'Hãy dâng cho Đức Giê-hô-va sự vinh quang', 6 | w_study_opening_song: 2, 7 | w_study_concluding_song: 159 8 | }, 9 | { 10 | w_study_date: '2025/03/10', 11 | w_study_date_locale: 'Bài học 2: Ngày 10-16 tháng 3 năm 2025', 12 | w_study_title: 'Người chồng hãy trân trọng vợ', 13 | w_study_opening_song: 132, 14 | w_study_concluding_song: 131 15 | }, 16 | { 17 | w_study_date: '2025/03/17', 18 | w_study_date_locale: 'Bài học 3: Ngày 17-23 tháng 3 năm 2025', 19 | w_study_title: 'Hãy đưa ra quyết định làm vui lòng Đức Giê-hô-va', 20 | w_study_opening_song: 35, 21 | w_study_concluding_song: 28 22 | }, 23 | { 24 | w_study_date: '2025/03/24', 25 | w_study_date_locale: 'Bài học 4: Ngày 24-30 tháng 3 năm 2025', 26 | w_study_title: 'Giá chuộc dạy chúng ta điều gì?', 27 | w_study_opening_song: 18, 28 | w_study_concluding_song: 107 29 | }, 30 | { 31 | w_study_date: '2025/03/31', 32 | w_study_date_locale: 'Bài học 5: Ngày 31 tháng 3 năm 2025–ngày 6 tháng 4 năm 2025', 33 | w_study_title: 'Chúng ta nhận lợi ích nào từ tình yêu thương của Đức Giê-hô-va?', 34 | w_study_opening_song: 108, 35 | w_study_concluding_song: 154 36 | } 37 | ] -------------------------------------------------------------------------------- /test/fixtures/w_VZ_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Lahatsoratsy Fianara 44: 6-12 Zanviè 2025', 5 | w_study_title: 'Akory ty Hiatrehanao ty Tsy Rariny?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Lahatsoratsy Fianara 45: 13-19 Zanviè 2025', 12 | w_study_title: 'Ty Hafatsy Farany baka amy Lahilahy Tsy Nivaliky Rey', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Lahatsoratsy Fianara 46: 20-26 Zanviè 2025', 19 | w_study_title: 'Nareo Rahalahy, Miezaha ho Lasa Mpikarakara Fiangona!', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Lahatsoratsy Fianara 47: 27 Zanviè 2025–2 Fevriè 2025', 26 | w_study_title: 'Nareo Rahalahy, Miezaha ho Lasa Anti-panahy!', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_X_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: '44. Studienartikel: 6. bis 12. Januar 2025', 5 | w_study_title: 'Wie wir mit Ungerechtigkeiten umgehen können', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: '45. Studienartikel: 13. bis 19. Januar 2025', 12 | w_study_title: 'Was wir aus den letzten Worten treuer Männer lernen können', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: '46. Studienartikel: 20. bis 26. Januar 2025', 19 | w_study_title: 'Hast du das Ziel, ein Dienstamtgehilfe zu werden?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: '47. Studienartikel: 27. Januar 2025 bis 2. Februar 2025', 26 | w_study_title: 'Hast du das Ziel, ein Ältester zu werden?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101 29 | } 30 | ] -------------------------------------------------------------------------------- /test/fixtures/w_YW_202501.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/03/03', 4 | w_study_date_locale: 'Igice cyo kwigwa cya 1: Itariki ya 3-9 Werurwe 2025', 5 | w_study_title: 'Duhe Yehova icyubahiro kimukwiriye', 6 | w_study_opening_song: 2, 7 | w_study_concluding_song: 159 8 | }, 9 | { 10 | w_study_date: '2025/03/10', 11 | w_study_date_locale: 'Igice cyo kwigwa cya 2: Itariki ya 10-16 Werurwe 2025', 12 | w_study_title: 'Uko abagabo bagaragaza ko bakunda abagore babo kandi ko babubaha', 13 | w_study_opening_song: 132, 14 | w_study_concluding_song: 131 15 | }, 16 | { 17 | w_study_date: '2025/03/17', 18 | w_study_date_locale: 'Igice cyo kwigwa cya 3: Itariki ya 17-23 Werurwe 2025', 19 | w_study_title: 'Wakora iki ngo ufate imyanzuro ishimisha Yehova?', 20 | w_study_opening_song: 35, 21 | w_study_concluding_song: 28 22 | }, 23 | { 24 | w_study_date: '2025/03/24', 25 | w_study_date_locale: 'Igice cyo kwigwa cya 4: Itariki ya 24-30 Werurwe 2025', 26 | w_study_title: 'Incungu itwigisha iki?', 27 | w_study_opening_song: 18, 28 | w_study_concluding_song: 107 29 | }, 30 | { 31 | w_study_date: '2025/03/31', 32 | w_study_date_locale: 'Igice cyo kwigwa cya 5: Itariki ya 31 Werurwe 2025–6 Mata 2025', 33 | w_study_title: 'Urukundo Yehova adukunda ruzatuma tubona imigisha', 34 | w_study_opening_song: 108, 35 | w_study_concluding_song: 154 36 | } 37 | ] -------------------------------------------------------------------------------- /test/fixtures/w_Z_202411.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | w_study_date: '2025/01/06', 4 | w_study_date_locale: 'Studieartikel 44: 6–12 januari 2025', 5 | w_study_title: 'Vad kan du göra om du blir orättvist behandlad?', 6 | w_study_opening_song: 33, 7 | w_study_concluding_song: 38, 8 | }, 9 | { 10 | w_study_date: '2025/01/13', 11 | w_study_date_locale: 'Studieartikel 45: 13–19 januari 2025', 12 | w_study_title: 'Vad kan vi lära oss av några trogna mäns avslutande ord?', 13 | w_study_opening_song: 138, 14 | w_study_concluding_song: 129, 15 | }, 16 | { 17 | w_study_date: '2025/01/20', 18 | w_study_date_locale: 'Studieartikel 46: 20–26 januari 2025', 19 | w_study_title: 'Kan du bli församlingstjänare?', 20 | w_study_opening_song: 49, 21 | w_study_concluding_song: 17, 22 | }, 23 | { 24 | w_study_date: '2025/01/27', 25 | w_study_date_locale: 'Studieartikel 47: 27 januari 2025–2 februari 2025', 26 | w_study_title: 'Kan du bli äldste?', 27 | w_study_opening_song: 103, 28 | w_study_concluding_song: 101, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /test/standardParsing/list.json: -------------------------------------------------------------------------------- 1 | [{ "language": "D", "issue": "202411" }] 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "ESNext" /* Specify what module code is generated. */, 5 | "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, 6 | "resolveJsonModule": true /* Enable importing .json files. */, 7 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, 8 | "emitDeclarationOnly": true /* Only output d.ts files and not JavaScript files. */, 9 | "outDir": "dist", /* Specify an output folder for all emitted files. */ 10 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 11 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 12 | "strict": true /* Enable all strict type-checking options. */, 13 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/browser/index.ts'], 6 | dts: { resolve: true, only: true }, 7 | format: ['esm'], // Ensure ESM output 8 | clean: true, 9 | outDir: 'dist', 10 | }, 11 | { 12 | entry: ['src/node/index.ts'], 13 | dts: { resolve: true, only: true }, 14 | format: ['esm'], // Ensure ESM output 15 | clean: true, 16 | outDir: 'dist/node', 17 | }, 18 | ]); 19 | --------------------------------------------------------------------------------