diff --git a/components/License/helper.ts b/components/License/helper.ts new file mode 100644 index 0000000..7fafefa --- /dev/null +++ b/components/License/helper.ts @@ -0,0 +1,53 @@ +import { FeatureAttitude, InfectionRange } from 'license-filter'; + +import { i18n } from '../../models/Translation'; + +type OptionValue = Record; + +type LicenseTips = Record; + +const options: string[] = [ + 'popularity', + 'reuseCondition', + 'infectionIntensity', + 'jurisdiction', + 'patentStatement', + 'patentRetaliation', + 'enhancedAttribution', + 'privacyLoophole', + 'marketingEndorsement', +]; + +export const optionValue = ({ t }: typeof i18n) => { + const optionValue = options.reduce((optionValue, option) => { + optionValue[option] = [ + { value: FeatureAttitude.Undefined, text: t('feature_attitude_undefined') }, + { value: FeatureAttitude.Positive, text: t('feature_attitude_positive') }, + { value: FeatureAttitude.Negative, text: t('feature_attitude_negative') }, + ]; + + return optionValue; + }, {} as OptionValue); + + optionValue.infectionRange = [ + { value: 0, text: t('infection_range_undefined') }, + { value: InfectionRange.Library, text: t('infection_range_library') }, + { value: InfectionRange.File, text: t('infection_range_file') }, + { value: InfectionRange.Module, text: t('infection_range_module') }, + ]; + + return optionValue; +}; + +export const licenseTips = ({ t }: typeof i18n): LicenseTips => ({ + popularity: [{ text: t('tip_popularity_0') }, { text: t('tip_popularity_1') }], + reuseCondition: [{ text: t('tip_reuse_condition') }], + infectionIntensity: [{ text: t('tip_infection_intensity') }], + jurisdiction: [{ text: t('tip_jurisdiction') }], + patentStatement: [{ text: t('tip_patent_statement') }], + patentRetaliation: [{ text: t('tip_patent_retaliation') }], + enhancedAttribution: [{ text: t('tip_enhanced_attribution') }], + privacyLoophole: [{ text: t('tip_privacy_loophole') }], + marketingEndorsement: [{ text: t('tip_marketing_endorsement') }], + infectionRange: [{ text: t('tip_infection_range') }], +}); \ No newline at end of file diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 15c57c8..bb03258 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -26,6 +26,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold', name: t('hackathon'), }, + { href: '/license-filter', name: t('license_filter') }, ]; export interface MainNavigatorProps { diff --git a/package.json b/package.json index 0a7807c..3d66b2e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "idea-react": "^2.0.0-rc.13", "koa": "^2.16.1", "koajax": "^3.1.2", + "license-filter": "^0.2.5", "marked": "^16.0.0", "mime": "^4.0.7", "mobx": "^6.13.7", @@ -28,6 +29,7 @@ "mobx-i18n": "^0.7.1", "mobx-lark": "^2.2.0", "mobx-react": "^9.2.0", + "mobx-react-helper": "^0.5.1", "mobx-restful": "^2.1.0", "mobx-restful-table": "^2.5.2", "next": "^15.3.5", diff --git a/pages/license-filter.tsx b/pages/license-filter.tsx new file mode 100644 index 0000000..8a2aca6 --- /dev/null +++ b/pages/license-filter.tsx @@ -0,0 +1,204 @@ +import { FeatureAttitude, filterLicenses, InfectionRange, License } from 'license-filter'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { ObservedComponent } from 'mobx-react-helper'; +import { Accordion, Button, ButtonGroup, Container, ProgressBar } from 'react-bootstrap'; + +import { PageHead } from '../components/Layout/PageHead'; +import { licenseTips, optionValue } from '../components/License/helper'; +import { i18n, I18nContext } from '../models/Translation'; + +interface List { + license: License; + score: number; +} + +const choiceSteps = [ + 'popularity', + 'reuseCondition', + 'infectionIntensity', + 'infectionRange', + 'jurisdiction', + 'patentStatement', + 'patentRetaliation', + 'enhancedAttribution', + 'privacyLoophole', + 'marketingEndorsement', +] as const; + +@observer +export default class LicenseTool extends ObservedComponent<{}, typeof i18n> { + static contextType = I18nContext; + + @observable + accessor stepIndex = 0; + + @observable + accessor keyIndex = 0; + + @observable + accessor filterOption = {}; + + @observable + accessor disableChoose = false; + + @observable + accessor lists: List[] = []; + + componentDidMount() { + if (this.stepIndex === choiceSteps.length) this.disableChoose = true; + } + + handleChoose = (value: string | null) => { + const { stepIndex, keyIndex, filterOption } = this; + + const choice = value ? +value : 0; + const key = choiceSteps[keyIndex]; + + const newObject = { ...filterOption, [key]: choice }; + const tempLists = filterLicenses(newObject); + + this.filterOption = newObject; + + this.lists = tempLists; + + this.stepIndex = stepIndex < choiceSteps.length ? stepIndex + 1 : stepIndex; + + this.keyIndex = keyIndex < choiceSteps.length - 1 ? keyIndex + 1 : keyIndex; + }; + + backToLast = () => { + const { stepIndex, keyIndex, filterOption, disableChoose } = this; + const choice = 0; + const key = choiceSteps[keyIndex]; + + const newObject = { ...filterOption, [key]: choice }; + const tempLists = filterLicenses(newObject); + + this.filterOption = newObject; + + this.stepIndex = + stepIndex === choiceSteps.length ? stepIndex - 2 : stepIndex > 0 ? stepIndex - 1 : stepIndex; + this.keyIndex = keyIndex > 0 ? keyIndex - 1 : keyIndex; + + if (disableChoose) this.disableChoose = false; + this.lists = tempLists; + }; + + render() { + const i18n = this.observedContext; + const { t } = i18n, + { keyIndex, disableChoose, lists } = this, + now = Math.ceil(100 / choiceSteps.length); + const percent = (keyIndex + 1) * now; + + return ( + + +

{t('license_tool_headline')}

+ +

{t('license_tool_description')}

+

{t('warn_info')}

+ +

+ {t('filter_option')}: {t(choiceSteps[keyIndex])} +

+ + {licenseTips(i18n)[choiceSteps[keyIndex]].map(({ text }) => ( +

{text}

+ ))} + + + + + {optionValue(i18n)[choiceSteps[keyIndex]].map(({ value, text }) => ( + + ))} + + + + {lists.map(({ license, score }, index) => ( + + + {license.name} {t('license_score')}: {score * 10} + + {this.renderInfo(license)} + + ))} + +
+ ); + } + + renderInfo({ link, feature }: License) { + const { t } = this.observedContext; + const judge = (attitude: FeatureAttitude) => + ({ + [FeatureAttitude.Positive]: t('attitude_positive'), + [FeatureAttitude.Negative]: t('attitude_negative'), + [FeatureAttitude.Undefined]: t('option_undefined'), + })[attitude] || t('option_undefined'); + + const judgeInfectionRange = (infectionRange: InfectionRange | undefined) => + infectionRange !== undefined + ? { + [InfectionRange.Library]: t('range_library'), + [InfectionRange.File]: t('range_file'), + [InfectionRange.Module]: t('range_module'), + }[infectionRange] + : t('option_undefined'); + + return ( + <> + + + + ); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5550455..80c4148 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: koajax: specifier: ^3.1.2 version: 3.1.2(core-js@3.44.0)(typescript@5.8.3) + license-filter: + specifier: ^0.2.5 + version: 0.2.5 marked: specifier: ^16.0.0 version: 16.0.0 @@ -59,6 +62,9 @@ importers: mobx-react: specifier: ^9.2.0 version: 9.2.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + mobx-react-helper: + specifier: ^0.5.1 + version: 0.5.1(mobx@6.13.7)(react@19.1.0)(typescript@5.8.3) mobx-restful: specifier: ^2.1.0 version: 2.1.0(core-js@3.44.0)(mobx@6.13.7)(typescript@5.8.3) @@ -3225,6 +3231,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + license-filter@0.2.5: + resolution: {integrity: sha512-xzKCeI9ax0k6/qALLhyXoJq1sbnmGHhjAX1AHr9ItDL9LF4jv97h64Z9iDyNTBQdhdr3BTI80VsWwTp0wpM1GQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3512,6 +3521,12 @@ packages: mobx: '>=6.11' react: '>=16' + mobx-react-helper@0.5.1: + resolution: {integrity: sha512-8jwR6LbPmC5s0tcmPz6CjXs1uarAcKjeTD+Oqbd7Vk4Ce49yDxeUOxG07VAcWZVnjnJXE0n79oG3z9c2XEEWTw==} + peerDependencies: + mobx: '>=6.11' + react: '>=16' + mobx-react-lite@4.1.0: resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} peerDependencies: @@ -8156,6 +8171,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + license-filter@0.2.5: + dependencies: + '@swc/helpers': 0.5.17 + lilconfig@3.1.3: {} lint-staged@16.1.2: @@ -8655,6 +8674,16 @@ snapshots: transitivePeerDependencies: - typescript + mobx-react-helper@0.5.1(mobx@6.13.7)(react@19.1.0)(typescript@5.8.3): + dependencies: + '@swc/helpers': 0.5.17 + lodash.isequalwith: 4.4.0 + mobx: 6.13.7 + react: 19.1.0 + web-utility: 4.4.3(typescript@5.8.3) + transitivePeerDependencies: + - typescript + mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: mobx: 6.13.7 diff --git a/translation/en-US.ts b/translation/en-US.ts index 6453bf0..65902fd 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -21,4 +21,61 @@ export default { // Volunteer page volunteer: 'Volunteer', online_volunteer: 'Online Volunteer', + + //License-tool Page + license_filter: 'Open Source License Selector', + feature_attitude_undefined: "I don't care", + feature_attitude_positive: 'I need', + feature_attitude_negative: "I don't need", + infection_range_library: 'Infection range to library', + infection_range_file: 'Infection range to file', + infection_range_module: 'Infection range to module', + infection_range_undefined: 'No request', + + tip_popularity_0: + 'Do you want to limit the result to a license agreement that is "popular and widely used, or has a broad community" as described by the Open Source Initiative (OSI)?', + tip_popularity_1: + 'This will sacrifice some less popular but perhaps useful features to ensure that the license becomes a mainstream license.', + tip_reuse_condition: + 'Do you want to set license conditions for code reuse? If not, your license will be one of the so-called "permissive" licenses.', + tip_infection_intensity: + 'Do you want to choose a strongly Copyleft licensing? When a software project contains some of your code, the project as a whole must be distributed under your license, if it is distributed at all. The effect of this will be that the source code for all additions made to the code will be available. If not,the parts of the project you originated from must be distributed under your license, if it is distributed at all. Other parts may be distributed under other licenses, even though they form part of a work with is - as a whole - a modified version of your code. The effect of this will be that the source code to some additions made to the code may not be available.', + tip_jurisdiction: 'Do you want your region to be the jurisdiction?', + tip_patent_statement: + 'Do you want to use a license agreement that explicitly grants patent rights (if any)?', + tip_patent_retaliation: + 'Do you want to use a license agreement that includes a patent retaliation clause? who brings legal action alleging that the licensed software embodies one of their software patents will lose the license you have granted to copy, use, adapt, and distribute the code. It is intended to dissuade people from bringing this kind of legal action.', + tip_enhanced_attribution: + 'Do you want to use a license agreement that specifies "enhanced attribution"? It must take a particular form and appear in specific instances, for example on the user interface of softwares every time it is run. ', + tip_privacy_loophole: + 'Do you want to use a license that addresses a "privacy loophole". Require that source code must also be released when services are provided over the Web or when code is deployed internally. The purpose of this is to ensure that all those who benefit from open source projects have a responsibility to give back to the community by sharing their improved and adapted versions.', + tip_marketing_endorsement: + "Do you want to allow promotional licenses? Avoid using the author's name to promote products or services based on the author's code. Such a restriction is intended to protect the authors reputation or prevent misleading publicity.", + tip_infection_range: + 'Which parts of the modified version do you want to allow for other licenses, with four options: module-level, file-level, library interface-level, and no requirements ?', + license_tool_headline: 'Open Source License Selector', + license_tool_description: + 'This tool is designed to help users understand their own preferences for free and open source software licensing agreements. Users must read these license agreements themselves. It is important to read and fully understand the license agreement you choose before applying it to your project. The classification of license types that support the operation of the tool will inevitably be somewhat reduced. Therefore, the output of the tool cannot and must not be taken as legal advice.', + warn_info: + 'Remember: You must read and understand the license agreement you choose', + filter_option: 'filter option', + option_undefined: 'Not required', + license_score: 'score', + popularity: 'Popularity', + reuseCondition: 'Reuse Condition', + infectionIntensity: 'Infection Intensity', + infectionRange: 'Infection Range', + jurisdiction: 'Jurisdiction', + patentStatement: 'Patent Statement', + patentRetaliation: 'Patent Retaliation', + enhancedAttribution: 'Enhanced Attribution', + privacyLoophole: 'Privacy Loophole', + marketingEndorsement: 'Marketing Endorsement', + license_detail: 'license detail', + attitude_positive: 'Yes', + attitude_negative: 'Yes', + range_library: 'library', + range_file: 'file', + range_module: 'module', + last_step: 'back', }; diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index 5929e1e..e39aea6 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -21,4 +21,60 @@ export default { // Volunteer page volunteer: '志愿者', online_volunteer: '线上志愿者', + + //License-tool Page + license_filter: '开源协议选择器', + feature_attitude_undefined: '我不在乎', + feature_attitude_positive: '我需要', + feature_attitude_negative: '我不需要', + infection_range_library: '传染范围到库', + infection_range_file: '传染范围到文件', + infection_range_module: '传染范围到模块', + infection_range_undefined: '不进行要求', + + tip_popularity_0: + '您是否希望将结果限制在开放源代码促进会(Open Source Initiative, 缩写: OSI)所描述的 "流行、广泛使用或拥有强大社群” 的许可证上?', + tip_popularity_1: + '为了确保许可协议为主流许可协议,将妥协放弃掉一些不那么主流但可能有用的特征。', + tip_reuse_condition: + '您想对代码的重复使用设置许可条件吗? 如果没有,您的许可证将属于所谓的 "宽松 (Permissive) " 许可证。所有自由与开源许可证都允许他人对您的代码进行修改,并将这些修改后的版本提供给他人。您的许可证可以对如何实现这一点提出条件,特别是在这些修改版本上可以使用哪些许可证。这些条件有助于保持代码的自由性,但也会使一些人不再重用您的代码。', + tip_infection_intensity: + '您是否想选择强互惠(强传染)的协议?当您选择强互惠(强传染)许可证时,任何使用、修改或分发您的代码的人都必须遵循相同的许可证要求。这意味着他们必须提供源代码,并将其代码以相同的许可证发布。这样,您的代码的开放性将被保护,并且任何人都可以获得您的代码的源代码,以便进行学习、改进和共享。强互惠(强传染)许可证确保了对整个项目的开放性和共享性,促进了开源社区的合作和创新。但是,选择强互惠(强传染)许可证可能会对某些开发者或组织造成限制。', + tip_jurisdiction: '您是否想将自己所在区域作为司法管辖区', + tip_patent_statement: '您是否想使用明确授予专利权的许可协议(如果有)', + tip_patent_retaliation: + '您是否想使用包含专利报复条款的许可协议。如果有人提起诉讼,声称开源软件侵犯了他们的软件专利,该条款将触发一种反制措施。根据这种条款,原告将失去使用、复制、改编和分发开源软件的许可。这意味着如果某人发起专利诉讼,他们将无法继续使用和分发被许可的开源代码。它旨在保护开源项目和贡献者免受专利诉讼的侵害,以维护项目的自由和开放性。', + tip_enhanced_attribution: + '您是否想使用指定“软件归属增强”的许可协议,必须以特定形式在特定情况下注明出处,例如每次运行软件时都必须在软件的用户界面上注明出处。所有自由或开源软件许可证都规定,分发或改编软件的任何人都必须在其分发的某处注明软件原作者。', + tip_privacy_loophole: + '您是否想使用解决“隐私漏洞”的许可协议,要求在通过网络提供服务或在内部部署代码时也必须发布源代码。这样做的目的是确保所有从开源项目中受益的人都有责任回馈社区,共享他们的改进和改编版本。', + tip_marketing_endorsement: + '您是否想使用禁止推广的许可协议,避免使用作者的姓名来推广基于作者代码的产品或服务。这样的限制,是为了保护作者的声誉或防止误导性宣传。', + tip_infection_range: + '您想对修改版的哪些部分可以适用其它许可协议,有四个选择: 模块级,文件级,库接口级,不进行要求', + + license_tool_headline: '开源许可证选择器', + license_tool_description: + '该工具旨在帮助用户理解他们自己对于自由和开源软件许可协议的偏好。用户必须自己阅读这些许可协议。在应用项目之前,阅读并完全理解许可协议非常重要。本工具借用的许可分类方式不能保证永远适用,因此,不可将本工具输出的信息作为法律依据。', + warn_info: '切记:必须阅读并理解您选择的许可协议', + filter_option: '筛选条件', + option_undefined: '不要求', + license_score: '评分', + popularity: '流行程度', + reuseCondition: '复用条件', + infectionIntensity: '互惠(传染)需求', + infectionRange: '传染范围', + jurisdiction: '法律管辖', + patentStatement: '专利声明', + patentRetaliation: '专利报复', + enhancedAttribution: '归属增强', + privacyLoophole: '隐私漏洞', + marketingEndorsement: '营销背书', + license_detail: '协议详情', + attitude_positive: '是', + attitude_negative: '否', + range_library: '库', + range_file: '文件', + range_module: '模块', + last_step: '上一步', }; diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index 0a13d6f..7cf1498 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -21,4 +21,60 @@ export default { // Volunteer page volunteer: '志工', online_volunteer: '線上志工', + + //License-tool Page + license_filter: '開源許可證選擇器', + feature_attitude_undefined: '我不在乎', + feature_attitude_positive: '我需要', + feature_attitude_negative: '我不需要', + infection_range_library: '傳染範圍到庫', + infection_range_file: '傳染範圍到檔案', + infection_range_module: '傳染範圍到模組', + infection_range_undefined: '不要求', + + tip_popularity_0: + '您是否希望將結果限制在開放原始碼促進會(Open Source Initiative, 縮寫: OSI)所描述的 "流行、廣泛使用或擁有強大社群」 的授權上?', + tip_popularity_1: + '這將以犧牲一些更冷僻但或許有用的特徵為代價來確保該許可協議成為“主流”協議。', + tip_reuse_condition: + '您想對程式碼的重複使用設定許可條件嗎? 如果沒有,您的許可證將屬於所謂的 "寬鬆 (Permissive) " 許可證。 所有自由與開源許可證都允許他人對您的程式碼進行修改,並將這些修改後的版本提供給他人。 您的許可證可以對如何實現這一點提出條件,特別是在這些修改版本上可以使用哪些許可證。 這些條件有助於保持程式碼的自由性,但也會使一些人不再重複使用您的程式碼。', + tip_infection_intensity: + '您是否想選擇強互惠(強傳染)的協議? 當您選擇強互惠(強傳染)許可證時,任何使用、修改或分發您的程式碼的人都必須遵循相同的授權要求。 這意味著他們必須提供原始程式碼,並將其程式碼以相同的許可證發布。 這樣,您的程式碼的開放性將被保護,並且任何人都可以獲得您的程式碼的原始程式碼,以便進行學習、改進和共享。 強互惠(強傳染)許可證確保了對整個專案的開放性和共享性,促進了開源社群的合作和創新。 但是,選擇強互惠(強傳染)許可證可能會對某些開發者或組織造成限制。', + tip_jurisdiction: '您是否想將自己所在區域作為司法管轄區', + tip_patent_statement: '您是否想使用明確授予專利權的許可協議(如果有)', + tip_patent_retaliation: + '您是否想使用包含專利報復條款的授權協議。 如果有人提起訴訟,聲稱開源軟體侵犯了他們的軟體專利,該條款將觸發一種反制措施。 根據這種條款,原告將失去使用、複製、改編和分發開源軟體的許可。 這意味著如果某人發起專利訴訟,他們將無法繼續使用和分發被授權的開源程式碼。 它旨在保護開源專案和貢獻者免受專利訴訟的侵害,以維護專案的自由和開放性。', + tip_enhanced_attribution: + '您是否想使用指定「軟體歸屬增強」的授權協議,必須以特定形式在特定情況下註明出處,例如每次執行軟體時都必須在軟體的使用者介面上註明出處。 所有自由或開源軟體授權都規定,分發或改編軟體的任何人都必須在其分發的某處註明軟體原作者。', + tip_privacy_loophole: + '您是否想使用解決「隱私漏洞」的許可協議,要求在透過網路提供服務或在內部部署程式碼時也必須發佈原始程式碼。 這樣做的目的是確保所有從開源專案中受益的人都有責任回饋社區,分享他們的改進和改編版本。', + tip_marketing_endorsement: + '您是否想使用禁止推廣的授權協議,避免使用作者的姓名來推廣基於作者代碼的產品或服務。 這樣的限制,是為了保護作者的聲譽或防止誤導性宣傳。', + tip_infection_range: + '您想對修改版的哪些部分可以適用其它許可協議,有 模組級,文件級,庫接口級,不進行要求四個選擇', + + license_tool_headline: '開源許可證選擇器', + license_tool_description: + '該工具旨在幫助用戶理解他們自己對於自由和開源軟件許可協議的偏好。用戶必須自己閱讀這些許可協議。在將許可協議適用於您的項目之前,閱讀並完全理解您選擇的許可協議是非常重要的。支撐該工具運行的許可類型分類,會不可避免地有些縮減。因此,不能也切不可將該工具的輸出信息視爲法律意見。', + warn_info: '切記:必須閱讀並理解您選擇的許可協議', + filter_option: '篩選條件', + option_undefined: '不要求', + license_score: '評分', + popularity: '流行程度', + reuseCondition: '復用條件', + infectionIntensity: '互惠(傳染)需求', + infectionRange: '傳染範圍', + jurisdiction: '法律管轄', + patentStatement: '專利聲明', + patentRetaliation: '專利報復', + enhancedAttribution: '歸屬增強', + privacyLoophole: '隱私漏洞', + marketingEndorsement: '營銷背書', + license_detail: '協議詳情', + attitude_positive: '是', + attitude_negative: '否', + range_library: '庫', + range_file: '檔案', + range_module: '模組', + last_step: '上一步', };