@jjlmoya/utils-civic 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +6 -0
  4. package/src/index.ts +2 -0
  5. package/src/pages/[locale]/[slug].astro +7 -4
  6. package/src/tests/locale_completeness.test.ts +1 -1
  7. package/src/tests/tool_validation.test.ts +1 -1
  8. package/src/tool/civic-priority-matrix/bibliography.astro +6 -0
  9. package/src/tool/civic-priority-matrix/bibliography.ts +12 -0
  10. package/src/tool/civic-priority-matrix/civic-priority-matrix.css +531 -0
  11. package/src/tool/civic-priority-matrix/component.astro +108 -0
  12. package/src/tool/civic-priority-matrix/contract.test.ts +16 -0
  13. package/src/tool/civic-priority-matrix/controller.ts +168 -0
  14. package/src/tool/civic-priority-matrix/dom-views.ts +123 -0
  15. package/src/tool/civic-priority-matrix/entry.ts +28 -0
  16. package/src/tool/civic-priority-matrix/evaluator.ts +13 -0
  17. package/src/tool/civic-priority-matrix/i18n/de.ts +52 -0
  18. package/src/tool/civic-priority-matrix/i18n/en.ts +73 -0
  19. package/src/tool/civic-priority-matrix/i18n/es.ts +52 -0
  20. package/src/tool/civic-priority-matrix/i18n/fr.ts +50 -0
  21. package/src/tool/civic-priority-matrix/i18n/id.ts +50 -0
  22. package/src/tool/civic-priority-matrix/i18n/it.ts +50 -0
  23. package/src/tool/civic-priority-matrix/i18n/ja.ts +121 -0
  24. package/src/tool/civic-priority-matrix/i18n/ko.ts +121 -0
  25. package/src/tool/civic-priority-matrix/i18n/nl.ts +45 -0
  26. package/src/tool/civic-priority-matrix/i18n/pl.ts +35 -0
  27. package/src/tool/civic-priority-matrix/i18n/pt.ts +32 -0
  28. package/src/tool/civic-priority-matrix/i18n/ru.ts +32 -0
  29. package/src/tool/civic-priority-matrix/i18n/sv.ts +32 -0
  30. package/src/tool/civic-priority-matrix/i18n/tr.ts +32 -0
  31. package/src/tool/civic-priority-matrix/i18n/zh.ts +121 -0
  32. package/src/tool/civic-priority-matrix/index.ts +11 -0
  33. package/src/tool/civic-priority-matrix/logic.test.ts +68 -0
  34. package/src/tool/civic-priority-matrix/logic.ts +124 -0
  35. package/src/tool/civic-priority-matrix/seo.astro +12 -0
  36. package/src/tool/civic-priority-matrix/storage.ts +50 -0
  37. package/src/tool/civic-priority-matrix/ui.ts +96 -0
  38. package/src/tool/participatory-budget-allocator/bibliography.astro +6 -0
  39. package/src/tool/participatory-budget-allocator/bibliography.ts +8 -0
  40. package/src/tool/participatory-budget-allocator/component.astro +61 -0
  41. package/src/tool/participatory-budget-allocator/contract.test.ts +13 -0
  42. package/src/tool/participatory-budget-allocator/controller.ts +108 -0
  43. package/src/tool/participatory-budget-allocator/dom-views.test.ts +14 -0
  44. package/src/tool/participatory-budget-allocator/dom-views.ts +58 -0
  45. package/src/tool/participatory-budget-allocator/entry.ts +27 -0
  46. package/src/tool/participatory-budget-allocator/evaluator.ts +24 -0
  47. package/src/tool/participatory-budget-allocator/i18n/de.ts +44 -0
  48. package/src/tool/participatory-budget-allocator/i18n/en.ts +79 -0
  49. package/src/tool/participatory-budget-allocator/i18n/es.ts +43 -0
  50. package/src/tool/participatory-budget-allocator/i18n/fr.ts +43 -0
  51. package/src/tool/participatory-budget-allocator/i18n/id.ts +43 -0
  52. package/src/tool/participatory-budget-allocator/i18n/it.ts +43 -0
  53. package/src/tool/participatory-budget-allocator/i18n/ja.ts +43 -0
  54. package/src/tool/participatory-budget-allocator/i18n/ko.ts +43 -0
  55. package/src/tool/participatory-budget-allocator/i18n/nl.ts +43 -0
  56. package/src/tool/participatory-budget-allocator/i18n/pl.ts +43 -0
  57. package/src/tool/participatory-budget-allocator/i18n/pt.ts +43 -0
  58. package/src/tool/participatory-budget-allocator/i18n/ru.ts +43 -0
  59. package/src/tool/participatory-budget-allocator/i18n/sv.ts +43 -0
  60. package/src/tool/participatory-budget-allocator/i18n/tr.ts +43 -0
  61. package/src/tool/participatory-budget-allocator/i18n/zh.ts +43 -0
  62. package/src/tool/participatory-budget-allocator/index.ts +11 -0
  63. package/src/tool/participatory-budget-allocator/logic.test.ts +48 -0
  64. package/src/tool/participatory-budget-allocator/logic.ts +146 -0
  65. package/src/tool/participatory-budget-allocator/participatory-budget-allocator.css +426 -0
  66. package/src/tool/participatory-budget-allocator/seo.astro +14 -0
  67. package/src/tool/participatory-budget-allocator/storage.ts +32 -0
  68. package/src/tool/participatory-budget-allocator/ui.ts +88 -0
  69. package/src/tools.ts +4 -0
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParticipatoryBudgetLocaleContent } from '../entry';
4
+ import type { ParticipatoryBudgetUI } from '../ui';
5
+
6
+ const ui: ParticipatoryBudgetUI = {
7
+ budgetLabel: 'Orçamento disponível', budgetHelp: 'Use uma única moeda e informe o limite que não pode ser ultrapassado.', proposalsHeading: 'Adicione propostas com uma pontuação comparável', proposalNameLabel: 'Nome da proposta', proposalCostLabel: 'Custo', proposalScoreLabel: 'Votos ou pontuação de benefício', addProposal: 'Adicionar proposta', removeProposal: 'Remover', calculateNote: 'A seleção é atualizada enquanto você edita as linhas. Cada proposta é financiada por inteiro ou fica de fora.', loadExample: 'Carregar exemplo', printAction: 'Imprimir ficha de revisão', resultHeading: 'Mosaico do orçamento', emptyResult: 'Adicione propostas para ver qual conjunto completo cabe no orçamento.', excludedLabel: 'Não financiada', spentLabel: 'Custo total', remainingLabel: 'Orçamento restante', scoreLabel: 'Pontuação total', proposalCountLabel: 'propostas', selectedStatus: 'Financiada', tooExpensiveStatus: 'Não cabe', notSelectedStatus: 'Ficou de fora', invalidStatus: 'Corrigir', reasonLabel: 'Por quê', methodHeading: 'Método aplicado', methodText: 'A ferramenta resolve um modelo de mochila 0/1. Cada proposta tem um custo e uma pontuação comparável. Ela busca combinações viáveis, maximiza a pontuação e depois usa menor custo, menos propostas e ordem de entrada como desempates determinísticos.', limitsHeading: 'O que esta ferramenta não faz', limitsText: 'Ela não decide se uma proposta é justa, legal, elegível, executável ou socialmente preferível. Também não substitui as regras do orçamento participativo nem uma votação.', warningsHeading: 'Casos extremos e alertas de dados', warningsText: 'Mantenha as pontuações comparáveis e use a mesma base de custos. Valores zero, nomes duplicados, estimativas incompletas, dependências e benefícios divisíveis exigem revisão humana.', noProposalsWarning: 'Nenhuma proposta foi informada.', noFeasibleWarning: 'Nenhuma proposta cabe no orçamento disponível.', invalidBudget: 'Informe um orçamento não negativo. O modelo do navegador aceita até 20.000,00 na moeda escolhida.', budgetTooLarge: 'O orçamento ultrapassa o limite do modelo de 20.000,00. Reduza o teto ou divida o exercício.', invalidProposal: 'Cada linha precisa de um nome, custo não negativo e pontuação não negativa.', costExceedsBudget: 'Este custo ultrapassa o orçamento disponível. A proposta não pode ser selecionada.', tieBreakLabel: 'Regra determinística', budgetUsedLabel: 'usado', budgetLeftLabel: 'restante', selectedTilesLabel: 'Blocos selecionados', allocationLabel: 'Capacidade orçamentária', statusSaved: 'Rascunho salvo localmente neste navegador.',
8
+ };
9
+ const faq = [
10
+ { question: 'O que este alocador otimiza?', answer: 'Ele escolhe um conjunto viável de propostas completas que maximiza a pontuação total informada sem ultrapassar o teto. Não decide se essa pontuação é justa ou significativa.' },
11
+ { question: 'Por que as propostas são tudo ou nada?', answer: 'A primeira versão usa um modelo 0/1. Cada proposta é financiada pelo custo completo ou excluída, o que se adapta a projetos cujo valor depende da entrega integral.' },
12
+ { question: 'O que acontece quando duas alocações têm a mesma pontuação?', answer: 'A ferramenta escolhe primeiro o menor custo total, depois menos propostas e, por fim, a ordem de entrada mais antiga. O desempate pode ser reproduzido.' },
13
+ { question: 'O resultado pode decidir um orçamento real?', answer: 'Não. É um cenário transparente. Verifique elegibilidade, dependências, capacidade de execução, base monetária e processo oficial.' },
14
+ ];
15
+ const howTo = [
16
+ { name: 'Defina o teto', text: 'Informe o valor disponível nesta rodada e mantenha a base monetária consistente.' },
17
+ { name: 'Adicione propostas completas', text: 'Inclua cada proposta uma vez com custo integral e votos ou pontuação de benefício comparáveis.' },
18
+ { name: 'Examine o mosaico', text: 'Revise conjunto financiado, pontuação, custo e orçamento restante conforme as linhas mudam.' },
19
+ { name: 'Leia as exclusões', text: 'Diferencie propostas que não cabem sozinhas das que perderam para uma combinação viável melhor.' },
20
+ { name: 'Faça verificações humanas', text: 'Compare o cenário com regras, dependências, capacidade e pessoas afetadas antes de uma reunião.' },
21
+ ];
22
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Alocador de orçamento participativo', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Selecione propostas públicas inteiras sob um limite orçamental fixo com uma regra de otimização transparente.', url: 'https://gamebob.dev/pt/alocador-orcamento-participativo', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
23
+ const faqSchema: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Distribuir um orçamento participativo fixo', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
25
+
26
+ export const content: ParticipatoryBudgetLocaleContent = { slug: 'alocador-orcamento-participativo', title: 'Alocador de orçamento participativo', description: 'Selecione propostas completas dentro de um orçamento fixo, maximize uma pontuação declarada e entenda o custo e o motivo de cada exclusão.', ui, seo: [
27
+ { type: 'title', text: 'Transforme um teto orçamental em um cenário reproduzível', level: 2 },
28
+ { type: 'paragraph', html: 'Uma rodada de orçamento público costuma ter mais propostas valiosas do que dinheiro disponível. Este alocador explicita a troca: informe o teto, adicione propostas completas e atribua a cada uma votos ou uma pontuação de benefício comparável. O resultado mostra um conjunto viável, sem esconder a escolha atrás de uma recomendação única.' },
29
+ { type: 'title', text: 'Como a seleção é calculada', level: 2 },
30
+ { type: 'paragraph', html: 'O modelo é uma mochila 0/1. O navegador compara combinações que ficam abaixo do teto e mantém a de maior pontuação total. Em caso de empate, aplica menor custo, menos propostas e ordem de entrada. A pontuação é o objetivo declarado, não uma medida independente de valor público.' },
31
+ { type: 'table', headers: ['Campo', 'Significado', 'Pergunta de revisão'], rows: [['Custo', 'Valor integral para entregar a proposta', 'Todas as linhas usam a mesma base?'], ['Pontuação', 'Votos ou valor de benefício informado', 'Os valores são comparáveis?'], ['Orçamento', 'Custo total máximo da seleção', 'O teto inclui os mesmos custos?'], ['Exclusão', 'Caro demais sozinho ou vencido por um conjunto melhor', 'A razão é matemática ou precisa de decisão?']] },
32
+ { type: 'title', text: 'Prepare entradas que não enganem', level: 2 },
33
+ { type: 'list', items: ['Confirme a elegibilidade de todas as propostas na mesma rodada.', 'Use o custo integral ou etapas realmente independentes.', 'Registre como votos e pontuações foram produzidos.', 'Verifique dependências, manutenção, questões legais e capacidade separadamente.', 'Guarde a seleção e os motivos de exclusão como registro de revisão.'] },
34
+ { type: 'tip', title: 'Uma pontuação é um objetivo escolhido', html: 'Se representa votos, o resultado maximiza os votos totais. Se representa benefício, maximiza essa avaliação. Mudar a pontuação muda a pergunta, então conserve o método junto do resultado.' },
35
+ { type: 'title', text: 'Leia os conjuntos financiado e excluído', level: 2 },
36
+ { type: 'paragraph', html: 'As propostas financiadas mostram custo, pontuação e orçamento restante. Uma proposta que não cabe custa mais que o teto sozinha. Uma proposta excluída pode caber sozinha, mas não está na melhor combinação segundo o objetivo e o desempate escolhidos.' },
37
+ { type: 'title', text: 'Limites e próximas verificações', level: 2 },
38
+ { type: 'paragraph', html: 'O alocador não avalia justiça entre bairros, risco de execução, conflitos entre projetos ou elegibilidade legal. O modelo do navegador é limitado a 20.000,00 para manter a busca exata rápida.' },
39
+ { type: 'paragraph', html: 'Uma combinação com pontuação alta só é confiável quando a base dos custos e o método de pontuação também são claros.' },
40
+ { type: 'title', text: 'Use o orçamento restante para conversar', level: 2 },
41
+ { type: 'paragraph', html: 'Um saldo pode indicar que nenhuma proposta adicional melhora a seleção atual. Também pode apontar uma proposta elegível ausente ou uma reserva intencional.' },
42
+ { type: 'tip', title: 'Revise antes de decidir', html: 'Leia a seleção matemática junto das regras oficiais de participação e das vozes das pessoas afetadas.' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqSchema, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParticipatoryBudgetLocaleContent } from '../entry';
4
+ import type { ParticipatoryBudgetUI } from '../ui';
5
+
6
+ const ui: ParticipatoryBudgetUI = {
7
+ budgetLabel: 'Доступный бюджет', budgetHelp: 'Используйте одну валюту и укажите предел, который нельзя превышать.', proposalsHeading: 'Добавьте предложения с сопоставимой оценкой', proposalNameLabel: 'Название предложения', proposalCostLabel: 'Стоимость', proposalScoreLabel: 'Голоса или оценка пользы', addProposal: 'Добавить предложение', removeProposal: 'Удалить', calculateNote: 'Выбор обновляется при изменении строк. Каждое предложение либо финансируется полностью, либо исключается.', loadExample: 'Загрузить пример', printAction: 'Печать листа проверки', resultHeading: 'Мозаика бюджета', emptyResult: 'Добавьте предложения, чтобы увидеть полный набор, который помещается в бюджет.', excludedLabel: 'Не финансируется', spentLabel: 'Общая стоимость', remainingLabel: 'Остаток бюджета', scoreLabel: 'Общая оценка', proposalCountLabel: 'предложений', selectedStatus: 'Профинансировано', tooExpensiveStatus: 'Не помещается', notSelectedStatus: 'Исключено', invalidStatus: 'Нужно исправить', reasonLabel: 'Почему', methodHeading: 'Применённый метод', methodText: 'Инструмент решает задачу о рюкзаке 0/1. У каждого предложения есть стоимость и сопоставимая оценка. Сначала ищется допустимая комбинация с максимальной оценкой, затем применяются меньшая стоимость, меньшее число предложений и более ранний порядок ввода.', limitsHeading: 'Чего этот инструмент не делает', limitsText: 'Он не определяет, справедливо ли предложение, законно ли оно, допустимо ли, выполнимо ли или предпочтительнее для общества. Он не заменяет правила участия и голосование.', warningsHeading: 'Особые случаи и предупреждения', warningsText: 'Сохраняйте сопоставимость оценок и единую основу стоимости. Нулевые значения, повторяющиеся названия, неполные сметы, зависимости и делимые выгоды требуют проверки человеком.', noProposalsWarning: 'Предложения не введены.', noFeasibleWarning: 'Ни одно предложение не помещается в доступный бюджет.', invalidBudget: 'Введите неотрицательный бюджет. Модель браузера принимает до 20 000,00 в выбранной валюте.', budgetTooLarge: 'Бюджет превышает предел модели 20 000,00. Уменьшите предел или разделите упражнение.', invalidProposal: 'В каждой строке нужны название, неотрицательная стоимость и неотрицательная оценка.', costExceedsBudget: 'Эта стоимость превышает доступный бюджет. Предложение нельзя выбрать.', tieBreakLabel: 'Детерминированное правило', budgetUsedLabel: 'использовано', budgetLeftLabel: 'осталось', selectedTilesLabel: 'Выбранные блоки', allocationLabel: 'Ёмкость бюджета', statusSaved: 'Черновик сохранён локально в этом браузере.',
8
+ };
9
+ const faq = [
10
+ { question: 'Что оптимизирует этот распределитель?', answer: 'Он выбирает допустимый набор целых предложений, который максимизирует введённую сумму оценок и не превышает предел. Он не решает, справедлива ли сама оценка.' },
11
+ { question: 'Почему предложения выбираются целиком или не выбираются?', answer: 'Первая версия использует модель 0/1. Предложение финансируется по полной стоимости или исключается, что подходит проектам, полезным только после полного выполнения.' },
12
+ { question: 'Что происходит при одинаковой оценке?', answer: 'Сначала выбирается меньшая общая стоимость, затем меньшее число предложений, затем более ранний порядок ввода. Поэтому результат можно воспроизвести.' },
13
+ { question: 'Может ли результат решить настоящий бюджет?', answer: 'Нет. Это прозрачный сценарий. Проверьте допустимость, зависимости, возможности реализации, валютную основу и официальную процедуру.' },
14
+ ];
15
+ const howTo = [
16
+ { name: 'Задайте предел', text: 'Введите доступную сумму для этого раунда и используйте единую основу стоимости.' },
17
+ { name: 'Добавьте полные предложения', text: 'Введите каждое предложение один раз с полной стоимостью и сопоставимой оценкой голосов или пользы.' },
18
+ { name: 'Проверьте мозаику', text: 'Следите за профинансированным набором, оценкой, стоимостью и остатком при изменении строк.' },
19
+ { name: 'Прочитайте исключения', text: 'Отделите предложения, слишком дорогие сами по себе, от проигравших лучшей допустимой комбинации.' },
20
+ { name: 'Проведите проверку человеком', text: 'До обсуждения сравните сценарий с правилами, зависимостями, возможностями и интересами жителей.' },
21
+ ];
22
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Распределитель бюджета участия', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Выбирайте целые общественные предложения в рамках фиксированного бюджета по прозрачному правилу оптимизации.', url: 'https://gamebob.dev/ru/raspredelitel-participativnogo-byudzheta', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
23
+ const faqSchema: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Распределить фиксированный бюджет участия', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
25
+
26
+ export const content: ParticipatoryBudgetLocaleContent = { slug: 'raspredelitel-participativnogo-byudzheta', title: 'Распределитель бюджета участия', description: 'Выберите целые предложения в рамках фиксированного бюджета, максимизируйте заявленную оценку и поймите стоимость и причину каждого исключения.', ui, seo: [
27
+ { type: 'title', text: 'Превратите предел бюджета в воспроизводимый сценарий', level: 2 },
28
+ { type: 'paragraph', html: 'В раунде общественного бюджета часто бывает больше полезных предложений, чем доступных денег. Инструмент делает компромисс явным: укажите предел, добавьте полные предложения и задайте сопоставимые голоса или оценки пользы. Результат показывает допустимый набор, а не скрытую рекомендацию.' },
29
+ { type: 'title', text: 'Как рассчитывается выбор', level: 2 },
30
+ { type: 'paragraph', html: 'Модель является задачей о рюкзаке 0/1. Браузер сравнивает комбинации в пределах бюджета и сохраняет комбинацию с наибольшей суммой оценок. При равенстве используются меньшая стоимость, меньшее число предложений и порядок ввода. Оценка является выбранной целью, а не независимой мерой общественной ценности.' },
31
+ { type: 'table', headers: ['Поле', 'Значение', 'Вопрос для проверки'], rows: [['Стоимость', 'Полная сумма для выполнения предложения', 'Все строки используют одну основу?'], ['Оценка', 'Введённые голоса или польза', 'Значения сопоставимы?'], ['Бюджет', 'Максимальная общая стоимость выбора', 'Предел включает те же расходы?'], ['Исключение', 'Слишком дорого само по себе или проиграло лучшему набору', 'Причина математическая или требует решения?']] },
32
+ { type: 'title', text: 'Подготовьте данные без вводящих в заблуждение допущений', level: 2 },
33
+ { type: 'list', items: ['Проверьте допустимость всех предложений для одного раунда.', 'Используйте полную стоимость или действительно независимые этапы.', 'Запишите происхождение голосов и оценок пользы.', 'Отдельно проверьте зависимости, обслуживание, право и возможности выполнения.', 'Сохраните выбор и причины исключений как протокол проверки.'] },
34
+ { type: 'tip', title: 'Оценка является выбранной целью', html: 'Если это голоса, результат максимизирует их сумму. Если это польза, он максимизирует эту оценку. Изменение оценки меняет вопрос, поэтому храните метод вместе с результатом.' },
35
+ { type: 'title', text: 'Читайте профинансированные и исключённые наборы', level: 2 },
36
+ { type: 'paragraph', html: 'Профинансированные предложения показывают стоимость, оценку и остаток. Предложение, которое не помещается, превышает предел само по себе. Исключённое предложение может помещаться отдельно, но не входит в лучшую комбинацию при выбранной цели и правилах равенства.' },
37
+ { type: 'title', text: 'Ограничения и следующие проверки', level: 2 },
38
+ { type: 'paragraph', html: 'Распределитель не оценивает справедливость между районами, риск выполнения, конфликты проектов или юридическую допустимость. Для быстрой точной проверки модель ограничена бюджетом 20 000,00 в выбранной валюте.' },
39
+ { type: 'paragraph', html: 'Комбинация с высокой оценкой надёжна только тогда, когда ясны основа стоимости и способ выставления оценок.' },
40
+ { type: 'title', text: 'Используйте остаток бюджета для обсуждения', level: 2 },
41
+ { type: 'paragraph', html: 'Остаток может означать, что дополнительное предложение не улучшает текущий выбор. Он также может указывать на пропущенное допустимое предложение или намеренный резерв.' },
42
+ { type: 'tip', title: 'Проверьте перед решением', html: 'Читайте математический выбор вместе с официальными правилами участия и голосами затронутых жителей.' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqSchema, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParticipatoryBudgetLocaleContent } from '../entry';
4
+ import type { ParticipatoryBudgetUI } from '../ui';
5
+
6
+ const ui: ParticipatoryBudgetUI = {
7
+ budgetLabel: 'Tillgänglig budget', budgetHelp: 'Använd en valuta och ange taket som inte får överskridas.', proposalsHeading: 'Lägg till förslag med jämförbar poäng', proposalNameLabel: 'Förslagets namn', proposalCostLabel: 'Kostnad', proposalScoreLabel: 'Röster eller nyttopoäng', addProposal: 'Lägg till förslag', removeProposal: 'Ta bort', calculateNote: 'Urvalet uppdateras när du redigerar raderna. Varje förslag finansieras helt eller lämnas utanför.', loadExample: 'Läs in exempel', printAction: 'Skriv ut granskningsblad', resultHeading: 'Budgetmosaik', emptyResult: 'Lägg till förslag för att se vilket komplett paket som ryms i budgeten.', excludedLabel: 'Inte finansierat', spentLabel: 'Total kostnad', remainingLabel: 'Återstående budget', scoreLabel: 'Total poäng', proposalCountLabel: 'förslag', selectedStatus: 'Finansierat', tooExpensiveStatus: 'Ryms inte', notSelectedStatus: 'Bortvalt', invalidStatus: 'Behöver rättas', reasonLabel: 'Varför', methodHeading: 'Använd metod', methodText: 'Verktyget löser en 0/1-ryggsäcksmodell. Varje förslag har en kostnad och en jämförbar poäng. Det söker möjliga kombinationer, maximerar poängen och använder sedan lägre kostnad, färre förslag och tidigare inmatningsordning som bestämda skiljeregler.', limitsHeading: 'Det här gör verktyget inte', limitsText: 'Det avgör inte om ett förslag är rättvist, lagligt, berättigat, genomförbart eller socialt bättre. Det ersätter inte regler för medborgarbudget eller en omröstning.', warningsHeading: 'Gränsfall och datavarningar', warningsText: 'Håll poäng jämförbara och använd samma kostnadsgrund. Nollor, dubbla namn, ofullständiga uppskattningar, beroenden och delbara nyttor kräver mänsklig granskning.', noProposalsWarning: 'Inga förslag har angetts.', noFeasibleWarning: 'Inget förslag ryms i den tillgängliga budgeten.', invalidBudget: 'Ange en icke-negativ budget. Webbläsarmodellen accepterar upp till 20 000,00 i vald valuta.', budgetTooLarge: 'Budgeten överstiger modellens gräns på 20 000,00. Sänk taket eller dela upp övningen.', invalidProposal: 'Varje rad behöver ett namn, en icke-negativ kostnad och en icke-negativ poäng.', costExceedsBudget: 'Den här kostnaden överstiger den tillgängliga budgeten. Förslaget kan inte väljas.', tieBreakLabel: 'Bestämd regel', budgetUsedLabel: 'använt', budgetLeftLabel: 'kvar', selectedTilesLabel: 'Valda block', allocationLabel: 'Budgetkapacitet', statusSaved: 'Utkastet sparades lokalt i den här webbläsaren.',
8
+ };
9
+ const faq = [
10
+ { question: 'Vad optimerar den här fördelaren?', answer: 'Den väljer en genomförbar uppsättning hela förslag som maximerar den totala poäng du anger utan att överskrida taket. Den avgör inte om poängen är rättvis eller meningsfull.' },
11
+ { question: 'Varför är förslagen allt eller inget?', answer: 'Den första versionen använder en 0/1-modell. Ett förslag finansieras till full kostnad eller utesluts, vilket passar projekt vars nytta kräver ett helt genomförande.' },
12
+ { question: 'Vad händer när två budgetar får samma poäng?', answer: 'Först väljs lägre totalkostnad, sedan färre förslag och sist tidigare inmatningsordning. Därför går resultatet att återskapa.' },
13
+ { question: 'Kan resultatet bestämma en riktig budget?', answer: 'Nej. Det är ett transparent scenario. Kontrollera behörighet, beroenden, genomförandekapacitet, valuta och den officiella processen.' },
14
+ ];
15
+ const howTo = [
16
+ { name: 'Ange taket', text: 'Skriv in beloppet som finns i den här omgången och använd samma kostnadsgrund.' },
17
+ { name: 'Lägg till hela förslag', text: 'Skriv in varje förslag en gång med full kostnad och jämförbar röst- eller nyttopoäng.' },
18
+ { name: 'Granska mosaiken', text: 'Följ finansierade förslag, poäng, kostnad och återstående budget när raderna ändras.' },
19
+ { name: 'Läs uteslutningarna', text: 'Skilj förslag som är för dyra på egen hand från dem som förlorade mot en bättre genomförbar kombination.' },
20
+ { name: 'Gör mänskliga kontroller', text: 'Jämför scenariot med regler, beroenden, kapacitet och berörda personer före mötet.' },
21
+ ];
22
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Fördelare för medborgarbudget', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Välj hela offentliga förslag under en fast budgetgräns med en transparent optimeringsregel.', url: 'https://gamebob.dev/sv/fordelare-for-deltagande-budget', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
23
+ const faqSchema: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Fördela en fast medborgarbudget', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
25
+
26
+ export const content: ParticipatoryBudgetLocaleContent = { slug: 'fordelare-for-deltagande-budget', title: 'Fördelare för medborgarbudget', description: 'Välj hela förslag under en fast budgetgräns, maximera en angiven poäng och se kostnaden och skälet bakom varje uteslutning.', ui, seo: [
27
+ { type: 'title', text: 'Gör en budgetgräns till ett reproducerbart scenario', level: 2 },
28
+ { type: 'paragraph', html: 'I en offentlig budgetomgång finns ofta fler värdefulla förslag än pengar. Fördelaren gör avvägningen tydlig: ange taket, lägg till hela förslag och ge varje förslag jämförbara röster eller nyttopoäng. Resultatet visar ett möjligt paket i stället för att gömma urvalet bakom ett enda råd.' },
29
+ { type: 'title', text: 'Så beräknas urvalet', level: 2 },
30
+ { type: 'paragraph', html: 'Modellen är ett 0/1-ryggsäcksproblem. Webbläsaren jämför kombinationer som håller sig under taket och behåller den med högst total poäng. Vid lika poäng används lägre kostnad, färre förslag och tidigare inmatning. Poängen är det valda målet, inte ett oberoende mått på offentligt värde.' },
31
+ { type: 'table', headers: ['Fält', 'Betydelse', 'Kontrollfråga'], rows: [['Kostnad', 'Hela beloppet för att genomföra förslaget', 'Använder alla rader samma grund?'], ['Poäng', 'Angivna röster eller nyttovärde', 'Går värdena att jämföra?'], ['Budget', 'Högsta totalkostnad för urvalet', 'Omfattar taket samma kostnader?'], ['Uteslutning', 'För dyrt ensamt eller besegrat av ett bättre paket', 'Är skälet matematiskt eller politiskt?']] },
32
+ { type: 'title', text: 'Förbered indata utan missvisande antaganden', level: 2 },
33
+ { type: 'list', items: ['Bekräfta att alla förslag gäller samma budgetomgång.', 'Använd full genomförandekostnad eller verkligt självständiga steg.', 'Dokumentera hur röster och nyttopoäng togs fram.', 'Kontrollera beroenden, underhåll, juridik och kapacitet separat.', 'Spara urval och uteslutningsskäl som granskningsunderlag.'] },
34
+ { type: 'tip', title: 'En poäng är ett valt mål', html: 'Om poängen är röster maximerar resultatet rösterna. Om den är nytta maximerar det den bedömningen. När poängen ändras ändras frågan, så spara metoden tillsammans med resultatet.' },
35
+ { type: 'title', text: 'Läs finansierade och uteslutna paket', level: 2 },
36
+ { type: 'paragraph', html: 'Finansierade förslag visar kostnad, poäng och återstående budget. Ett förslag som inte ryms är dyrare än taket på egen hand. Ett bortvalt förslag kan rymmas ensamt men ingår inte i den bästa kombinationen enligt det valda målet och skiljereglerna.' },
37
+ { type: 'title', text: 'Begränsningar och nästa kontroller', level: 2 },
38
+ { type: 'paragraph', html: 'Fördelaren bedömer inte rättvisa mellan områden, genomföranderisk, projektkonflikter eller juridisk behörighet. Modellen är begränsad till 20 000,00 för att den exakta sökningen ska vara snabb.' },
39
+ { type: 'paragraph', html: 'En kombination med hög poäng är bara tillförlitlig när kostnadsgrunden och poängmetoden också är tydliga.' },
40
+ { type: 'title', text: 'Använd den återstående budgeten i samtalet', level: 2 },
41
+ { type: 'paragraph', html: 'Ett överskott kan betyda att inget ytterligare förslag förbättrar det aktuella urvalet. Det kan också visa att ett berättigat förslag saknas eller att pengar avsiktligt sparas.' },
42
+ { type: 'tip', title: 'Kontrollera före beslut', html: 'Läs det matematiska urvalet tillsammans med officiella deltaganderegler och rösterna från berörda personer.' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqSchema, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParticipatoryBudgetLocaleContent } from '../entry';
4
+ import type { ParticipatoryBudgetUI } from '../ui';
5
+
6
+ const ui: ParticipatoryBudgetUI = {
7
+ budgetLabel: 'Kullanılabilir bütçe', budgetHelp: 'Tek bir para birimi kullanın ve aşılmaması gereken üst sınırı girin.', proposalsHeading: 'Karşılaştırılabilir puanla öneri ekleyin', proposalNameLabel: 'Öneri adı', proposalCostLabel: 'Maliyet', proposalScoreLabel: 'Oy veya fayda puanı', addProposal: 'Öneri ekle', removeProposal: 'Kaldır', calculateNote: 'Satırları düzenledikçe seçim güncellenir. Her öneri ya tamamen finanse edilir ya da dışarıda kalır.', loadExample: 'Örneği yükle', printAction: 'İnceleme sayfasını yazdır', resultHeading: 'Bütçe mozaiği', emptyResult: 'Bütçeye sığan tam kombinasyonu görmek için öneri ekleyin.', excludedLabel: 'Finanse edilmedi', spentLabel: 'Toplam maliyet', remainingLabel: 'Kalan bütçe', scoreLabel: 'Toplam puan', proposalCountLabel: 'öneri', selectedStatus: 'Finanse edildi', tooExpensiveStatus: 'Sığmıyor', notSelectedStatus: 'Dışarıda', invalidStatus: 'Düzeltilmeli', reasonLabel: 'Neden', methodHeading: 'Uygulanan yöntem', methodText: 'Araç 0/1 sırt çantası modelini çözer. Her önerinin bir maliyeti ve karşılaştırılabilir bir puanı vardır. Uygun kombinasyonları arar, puanı en yükseğe çıkarır ve eşitlikte daha düşük maliyeti, daha az öneriyi ve daha erken giriş sırasını kullanır.', limitsHeading: 'Bu araç ne yapmaz', limitsText: 'Bir önerinin adil, yasal, uygun, uygulanabilir veya toplum için daha iyi olup olmadığına karar vermez. Katılımcı bütçeleme kurallarının ya da oylamanın yerini tutmaz.', warningsHeading: 'Sınır durumları ve veri uyarıları', warningsText: 'Puanları karşılaştırılabilir tutun ve aynı maliyet temelini kullanın. Sıfırlar, yinelenen adlar, eksik tahminler, bağımlılıklar ve bölünebilir faydalar insan incelemesi gerektirir.', noProposalsWarning: 'Hiç öneri girilmedi.', noFeasibleWarning: 'Kullanılabilir bütçeye sığan öneri yok.', invalidBudget: 'Negatif olmayan bir bütçe girin. Tarayıcı modeli seçilen para biriminde 20.000,00 değerine kadar çalışır.', budgetTooLarge: 'Bütçe 20.000,00 model sınırını aşıyor. Üst sınırı azaltın veya çalışmayı bölün.', invalidProposal: 'Her satırda ad, negatif olmayan maliyet ve negatif olmayan puan bulunmalıdır.', costExceedsBudget: 'Bu maliyet kullanılabilir bütçeyi aşıyor. Öneri seçilemez.', tieBreakLabel: 'Belirleyici kural', budgetUsedLabel: 'kullanıldı', budgetLeftLabel: 'kaldı', selectedTilesLabel: 'Seçilen bloklar', allocationLabel: 'Bütçe kapasitesi', statusSaved: 'Taslak bu tarayıcıya yerel olarak kaydedildi.',
8
+ };
9
+ const faq = [
10
+ { question: 'Bu dağıtıcı neyi optimize eder?', answer: 'Üst sınırı aşmadan girdiğiniz toplam puanı en yükseğe çıkaran, uygulanabilir ve tam öneri kümesini seçer. Puanın adil veya anlamlı olup olmadığına karar vermez.' },
11
+ { question: 'Öneriler neden ya tamamen seçilir ya da hiç seçilmez?', answer: 'İlk sürüm 0/1 modeli kullanır. Her öneri tam maliyetiyle uygulanır veya dışarıda bırakılır; bu, faydası tamamlanmasına bağlı projelere uyar.' },
12
+ { question: 'İki dağıtımın puanı aynıysa ne olur?', answer: 'Önce daha düşük toplam maliyet, sonra daha az öneri ve son olarak daha erken giriş sırası seçilir. Sonuç başka bir inceleyici tarafından tekrarlanabilir.' },
13
+ { question: 'Bu sonuç gerçek bir bütçeyi belirleyebilir mi?', answer: 'Hayır. Bu şeffaf bir senaryodur. Uygunluğu, bağımlılıkları, uygulama kapasitesini, para birimi temelini ve resmî süreci kontrol edin.' },
14
+ ];
15
+ const howTo = [
16
+ { name: 'Üst sınırı belirleyin', text: 'Bu turdaki kullanılabilir tutarı girin ve maliyet temelini tutarlı tutun.' },
17
+ { name: 'Tam öneriler ekleyin', text: 'Her öneriyi bir kez, tam maliyeti ve karşılaştırılabilir oy veya fayda puanıyla girin.' },
18
+ { name: 'Mozaiği inceleyin', text: 'Satırlar değiştikçe finanse edilen kümeyi, puanı, maliyeti ve kalan bütçeyi izleyin.' },
19
+ { name: 'Dışarıda kalanları okuyun', text: 'Tek başına fazla pahalı olanları daha iyi bir uygulanabilir kombinasyona kaybedenlerden ayırın.' },
20
+ { name: 'İnsan kontrollerini uygulayın', text: 'Toplantıda kullanmadan önce senaryoyu kurallar, bağımlılıklar, kapasite ve etkilenen kişilerle karşılaştırın.' },
21
+ ];
22
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Katılımcı bütçe dağıtıcısı', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Sabit bütçe sınırı altında tam kamu önerilerini şeffaf bir optimizasyon kuralıyla seçin.', url: 'https://gamebob.dev/tr/katilimci-butce-paylastirici', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
23
+ const faqSchema: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Sabit katılımcı bütçesini dağıtma', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
25
+
26
+ export const content: ParticipatoryBudgetLocaleContent = { slug: 'katilimci-butce-paylastirici', title: 'Katılımcı bütçe dağıtıcısı', description: 'Sabit bütçe sınırı altında tam önerileri seçin, belirlenen puanı en yükseğe çıkarın ve her dışlama için maliyeti ve nedeni görün.', ui, seo: [
27
+ { type: 'title', text: 'Bütçe sınırını tekrarlanabilir bir senaryoya dönüştürün', level: 2 },
28
+ { type: 'paragraph', html: 'Kamu bütçesi turlarında kullanılabilir paradan daha fazla değerli öneri bulunur. Bu dağıtıcı sınırı girmenizi, tam önerileri eklemenizi ve her birine karşılaştırılabilir oy veya fayda puanı vermenizi sağlar. Sonuç, seçimi tek bir gizli tavsiyenin arkasına saklamak yerine uygulanabilir bir küme gösterir.' },
29
+ { type: 'title', text: 'Seçim nasıl hesaplanır', level: 2 },
30
+ { type: 'paragraph', html: 'Model 0/1 sırt çantası problemidir. Tarayıcı sınır içinde kalan kombinasyonları karşılaştırır ve toplam puanı en yüksek olanı tutar. Eşitlikte daha düşük maliyet, daha az öneri ve daha erken giriş uygulanır. Puan, seçtiğiniz hedeftir; kamu değerinin bağımsız ölçüsü değildir.' },
31
+ { type: 'table', headers: ['Alan', 'Anlamı', 'İnceleme sorusu'], rows: [['Maliyet', 'Öneriyi uygulamak için gereken tam tutar', 'Tüm satırlar aynı temeli kullanıyor mu?'], ['Puan', 'Girilen oy veya fayda değeri', 'Değerler karşılaştırılabilir mi?'], ['Bütçe', 'Seçilen önerilerin azami toplam maliyeti', 'Sınır aynı maliyetleri kapsıyor mu?'], ['Dışlama', 'Tek başına pahalı veya daha iyi kümeye yenilmiş', 'Neden matematiksel mi, politika incelemesi mi?']] },
32
+ { type: 'title', text: 'Yanıltmayan girdiler hazırlayın', level: 2 },
33
+ { type: 'list', items: ['Tüm önerilerin aynı tur için uygun olduğunu doğrulayın.', 'Tam uygulama maliyeti veya gerçekten bağımsız aşamalar kullanın.', 'Oyların ve fayda puanlarının nasıl üretildiğini kaydedin.', 'Bağımlılıkları, bakımı, hukuki konuları ve kapasiteyi ayrı kontrol edin.', 'Seçimi ve dışlama nedenlerini inceleme kaydı olarak saklayın.'] },
34
+ { type: 'tip', title: 'Puan seçilmiş bir hedeftir', html: 'Puan oyları temsil ediyorsa sonuç toplam oyları, faydayı temsil ediyorsa o değerlendirmeyi en yükseğe çıkarır. Puanı değiştirmek soruyu da değiştirir; yöntemi sonuçla birlikte saklayın.' },
35
+ { type: 'title', text: 'Finanse edilen ve dışlanan kümeleri okuyun', level: 2 },
36
+ { type: 'paragraph', html: 'Finanse edilen öneriler maliyeti, puanı ve kalan bütçeyi gösterir. Sığmayan öneri tek başına bile sınırdan pahalıdır. Dışlanan öneri tek başına sığabilir, ancak seçilen hedef ve eşitlik kuralına göre en iyi kombinasyona girmez.' },
37
+ { type: 'title', text: 'Sınırlar ve sonraki kontroller', level: 2 },
38
+ { type: 'paragraph', html: 'Dağıtıcı bölgeler arası adaleti, uygulama riskini, proje çatışmalarını veya hukuki uygunluğu değerlendirmez. Kesin aramayı hızlı tutmak için tarayıcı modeli 20.000,00 ile sınırlıdır.' },
39
+ { type: 'paragraph', html: 'Yüksek puanlı bir kombinasyon ancak maliyet temeli ve puanlama yöntemi açıksa güvenilirdir.' },
40
+ { type: 'title', text: 'Kalan bütçeyi konuşma konusu yapın', level: 2 },
41
+ { type: 'paragraph', html: 'Kalan tutar, ek bir önerinin mevcut seçimi iyileştirmediğini gösterebilir. Eksik bir uygun öneriye veya bilinçli bir rezerve de işaret edebilir.' },
42
+ { type: 'tip', title: 'Karar vermeden kontrol edin', html: 'Matematiksel seçimi resmî katılım kuralları ve etkilenen kişilerin görüşleriyle birlikte okuyun.' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqSchema, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParticipatoryBudgetLocaleContent } from '../entry';
4
+ import type { ParticipatoryBudgetUI } from '../ui';
5
+
6
+ const ui: ParticipatoryBudgetUI = {
7
+ budgetLabel: '可用预算', budgetHelp: '使用一种货币,并输入不能超过的上限。', proposalsHeading: '添加可比较评分的提案', proposalNameLabel: '提案名称', proposalCostLabel: '成本', proposalScoreLabel: '票数或收益评分', addProposal: '添加提案', removeProposal: '删除', calculateNote: '编辑各行时,选择结果会自动更新。每个提案要么获得全额资助,要么被排除。', loadExample: '加载示例', printAction: '打印审核单', resultHeading: '预算拼图', emptyResult: '添加提案,查看哪些完整组合能放入预算。', excludedLabel: '未获资助', spentLabel: '总成本', remainingLabel: '剩余预算', scoreLabel: '总评分', proposalCountLabel: '个提案', selectedStatus: '已资助', tooExpensiveStatus: '无法放入', notSelectedStatus: '已排除', invalidStatus: '需要修正', reasonLabel: '原因', methodHeading: '使用的方法', methodText: '工具解决0/1背包模型。每个提案都有成本和可比较的评分。它搜索可行组合,最大化总评分,然后依次使用更低成本、更少提案和更早输入顺序作为确定性平局规则。', limitsHeading: '本工具不做什么', limitsText: '它不会判断提案是否公平、合法、符合资格、可执行或更有社会价值,也不能取代参与式预算规则或投票。', warningsHeading: '边界情况和数据提醒', warningsText: '保持评分含义一致,并使用同一成本口径。零值、重复名称、不完整估算、依赖关系和可拆分收益都需要人工审核。', noProposalsWarning: '尚未输入提案。', noFeasibleWarning: '没有提案能放入可用预算。', invalidBudget: '请输入非负预算。浏览器模型支持所选货币下最高20,000.00。', budgetTooLarge: '预算超过模型上限20,000.00。请降低上限或拆分练习。', invalidProposal: '每一行都需要名称、非负成本和非负评分。', costExceedsBudget: '该成本超过可用预算,提案无法被选择。', tieBreakLabel: '确定性规则', budgetUsedLabel: '已使用', budgetLeftLabel: '剩余', selectedTilesLabel: '已选区块', allocationLabel: '预算容量', statusSaved: '草稿已保存在此浏览器中。',
8
+ };
9
+ const faq = [
10
+ { question: '这个分配工具优化什么?', answer: '它选择一组可行的完整提案,在不超过上限的情况下最大化你输入的总评分。它不会判断评分是否公平或有意义。' },
11
+ { question: '为什么提案只能全选或不选?', answer: '第一版使用0/1模型。每个提案要么按完整成本执行,要么被排除,适合必须完整交付才有价值的项目。' },
12
+ { question: '两个分配方案评分相同时怎么办?', answer: '工具先选择总成本较低的方案,再选择提案较少的方案,最后选择输入顺序较早的方案,因此结果可以复现。' },
13
+ { question: '这个结果能决定真实预算吗?', answer: '不能。这只是一个透明场景。请检查资格、依赖关系、执行能力、货币口径和正式流程。' },
14
+ ];
15
+ const howTo = [
16
+ { name: '设定上限', text: '输入本轮可用金额,并保持成本口径一致。' },
17
+ { name: '添加完整提案', text: '每个提案输入一次,填写完整成本和可比较的票数或收益评分。' },
18
+ { name: '查看拼图', text: '随着各行变化,检查已资助组合、评分、成本和剩余预算。' },
19
+ { name: '阅读排除原因', text: '区分单独就太贵的提案和输给更好可行组合的提案。' },
20
+ { name: '进行人工检查', text: '在会议中使用前,把场景与规则、依赖关系、能力和受影响人群进行比较。' },
21
+ ];
22
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: '参与式预算分配工具', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: '在固定预算上限下选择完整公共提案,并使用透明的优化规则。', url: 'https://gamebob.dev/zh/participatory-budget-allocator', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
23
+ const faqSchema: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: '分配固定的参与式预算', step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) };
25
+
26
+ export const content: ParticipatoryBudgetLocaleContent = { slug: 'participatory-budget-allocator', title: '参与式预算分配工具', description: '在固定预算下选择完整提案,最大化声明的评分,并查看每项排除的成本和原因。', ui, seo: [
27
+ { type: 'title', text: '把预算上限变成可复现的场景', level: 2 },
28
+ { type: 'paragraph', html: '公共预算轮次中,有价值的提案往往多于可用资金。这个分配工具要求你输入上限、添加完整提案,并为每项提案提供可比较的票数或收益评分,从而让取舍清晰可见。结果展示的是可行组合,而不是隐藏在单一建议之后的选择。' },
29
+ { type: 'title', text: '选择结果如何计算', level: 2 },
30
+ { type: 'paragraph', html: '模型是0/1背包问题。浏览器比较不超过上限的组合,保留总评分最高的组合。评分相同时依次使用更低成本、更少提案和更早输入顺序。评分是你声明的目标,不是独立的公共价值指标。' },
31
+ { type: 'table', headers: ['字段', '含义', '审核问题'], rows: [['成本', '交付提案所需的完整金额', '所有行是否采用同一口径?'], ['评分', '用户输入的票数或收益值', '这些数值可以比较吗?'], ['预算', '所选提案的最大总成本', '上限是否包含相同成本?'], ['排除', '单独太贵或输给更好组合', '原因是数学上的还是需要政策审核?']] },
32
+ { type: 'title', text: '准备不会误导人的输入', level: 2 },
33
+ { type: 'list', items: ['确认所有提案都适用于同一预算轮次。', '使用完整交付成本,或使用真正独立的阶段。', '记录票数和收益评分的产生方式。', '分别检查依赖关系、维护、法律事项和执行能力。', '把选择结果和排除原因保存为审核记录。'] },
34
+ { type: 'tip', title: '评分是选择的目标', html: '如果评分代表票数,结果会最大化总票数;如果代表收益,就会最大化收益评价。改变评分就改变了问题,因此要把评分方法和结果一起保存。' },
35
+ { type: 'title', text: '阅读已资助和已排除的组合', level: 2 },
36
+ { type: 'paragraph', html: '已资助提案会显示成本、评分和剩余预算。无法放入的提案单独就超过上限。已排除提案可能单独可以放入,但按照选定目标和平局规则,不属于最佳组合。' },
37
+ { type: 'title', text: '限制和后续检查', level: 2 },
38
+ { type: 'paragraph', html: '工具不会判断地区之间的公平性、执行风险、项目冲突或法律资格。为保持精确搜索的响应速度,浏览器模型限制在所选货币下20,000.00。' },
39
+ { type: 'paragraph', html: '高评分组合只有在成本口径和评分方法都清楚时才值得信任。' },
40
+ { type: 'title', text: '把剩余预算变成讨论议题', level: 2 },
41
+ { type: 'paragraph', html: '剩余金额可能表示没有其他提案能改善当前选择,也可能表示遗漏了合资格提案,或预算被有意保留。' },
42
+ { type: 'tip', title: '决定前先检查', html: '把数学选择与正式参与规则以及受影响人群的意见一起阅读。' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqSchema, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { participatoryBudgetAllocator } from './entry';
3
+
4
+ export * from './entry';
5
+ export * from './logic';
6
+ export const PARTICIPATORY_BUDGET_ALLOCATOR_TOOL: ToolDefinition = {
7
+ entry: participatoryBudgetAllocator,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { allocateBudget, type AllocationInput } from './logic';
3
+
4
+ const base: AllocationInput = { budget: 10, proposals: [{ name: 'A', cost: 6, score: 9 }, { name: 'B', cost: 4, score: 5 }, { name: 'C', cost: 5, score: 8 }] };
5
+
6
+ describe('participatory budget allocation', () => {
7
+ it('selects a whole proposal combination with the highest score', () => {
8
+ const result = allocateBudget(base);
9
+ expect(result.selected.map((proposal) => proposal.name)).toEqual(['A', 'B']);
10
+ expect(result.totals).toMatchObject({ spent: 10, remaining: 0, score: 14 });
11
+ });
12
+
13
+ it('uses lower cost and then input order for equal scores', () => {
14
+ const result = allocateBudget({ budget: 6, proposals: [{ name: 'First', cost: 6, score: 10 }, { name: 'Second', cost: 5, score: 10 }, { name: 'Third', cost: 5, score: 10 }] });
15
+ expect(result.selected.map((proposal) => proposal.name)).toEqual(['Second']);
16
+ });
17
+
18
+ it('handles empty proposals and no feasible proposal', () => {
19
+ expect(allocateBudget({ budget: 0, proposals: [] }).warnings).toContain('no-proposals');
20
+ const result = allocateBudget({ budget: 3, proposals: [{ name: 'Large', cost: 4, score: 10 }] });
21
+ expect(result.warnings).toContain('no-feasible-proposal');
22
+ expect(result.excluded[0]).toMatchObject({ status: 'too-expensive' });
23
+ });
24
+
25
+ it('supports decimal costs without floating point drift', () => {
26
+ const result = allocateBudget({ budget: 10.5, proposals: [{ name: 'One', cost: 5.25, score: 3 }, { name: 'Two', cost: 5.25, score: 4 }, { name: 'Three', cost: 5.3, score: 6 }] });
27
+ expect(result.totals.spent).toBe(10.5);
28
+ expect(result.selected.map((proposal) => proposal.name)).toEqual(['One', 'Two']);
29
+ });
30
+
31
+ it('rejects invalid fields and impossible budget sizes', () => {
32
+ expect(allocateBudget({ budget: -1, proposals: [] }).errors).toContain('invalid-budget');
33
+ expect(allocateBudget({ budget: 20_000.01, proposals: [] }).errors).toContain('budget-too-large');
34
+ expect(allocateBudget({ budget: 10, proposals: [{ name: '', cost: -2, score: Number.NaN }] }).errors).toEqual(expect.arrayContaining(['missing-name', 'invalid-cost', 'invalid-score']));
35
+ });
36
+
37
+ it('keeps special characters and reports every non-selected reason', () => {
38
+ const result = allocateBudget({ budget: 5, proposals: [{ name: 'Café & youth', cost: 5, score: 1 }, { name: '<Bridge>', cost: 5, score: 2 }] });
39
+ expect(result.selected[0]?.name).toBe('<Bridge>');
40
+ expect(result.excluded[0]?.reason).toContain('remaining budget');
41
+ });
42
+
43
+ it('explains when a proposal exceeds the remaining budget with the selected set', () => {
44
+ const result = allocateBudget({ budget: 5000, proposals: [{ name: 'Community tool library', cost: 4200, score: 74 }, { name: 'Library ramp', cost: 1000, score: 20 }] });
45
+ expect(result.selected.map((proposal) => proposal.name)).toEqual(['Community tool library']);
46
+ expect(result.excluded[0]?.reason).toContain('remaining budget alongside the selected proposals');
47
+ });
48
+ });
@@ -0,0 +1,146 @@
1
+ export interface ProposalInput {
2
+ name: string;
3
+ cost: number;
4
+ score: number;
5
+ }
6
+
7
+ export interface AllocationInput {
8
+ budget: number;
9
+ proposals: ProposalInput[];
10
+ }
11
+
12
+ export type ProposalStatus = 'selected' | 'too-expensive' | 'not-selected' | 'invalid';
13
+
14
+ export interface ProposalResult extends ProposalInput {
15
+ index: number;
16
+ status: ProposalStatus;
17
+ reason: string;
18
+ }
19
+
20
+ export interface AllocationTotals {
21
+ budget: number;
22
+ spent: number;
23
+ remaining: number;
24
+ score: number;
25
+ selectedCount: number;
26
+ }
27
+
28
+ export interface AllocationResult {
29
+ valid: boolean;
30
+ errors: string[];
31
+ warnings: string[];
32
+ selected: ProposalResult[];
33
+ excluded: ProposalResult[];
34
+ allProposals: ProposalResult[];
35
+ totals: AllocationTotals;
36
+ tieBreak: string;
37
+ }
38
+
39
+ interface Candidate {
40
+ indices: number[];
41
+ cost: number;
42
+ score: number;
43
+ }
44
+
45
+ const MAX_BUDGET_CENTS = 2_000_000;
46
+ const TIE_BREAK = 'Maximize total score, then minimize total cost, then prefer fewer proposals and earlier input order.';
47
+
48
+ function isFiniteNonnegative(value: number): boolean {
49
+ return Number.isFinite(value) && value >= 0;
50
+ }
51
+
52
+ function decimalPlaces(value: number): number {
53
+ const text = String(value);
54
+ const decimal = text.split('.')[1];
55
+ return Math.min(decimal?.length ?? 0, 2);
56
+ }
57
+
58
+ function scaleValue(value: number, scale: number): number {
59
+ return Math.round(value * scale);
60
+ }
61
+
62
+ function compareIndices(left: number[], right: number[]): number {
63
+ const length = Math.min(left.length, right.length);
64
+ for (let position = 0; position < length; position += 1) {
65
+ if (left[position] !== right[position]) {
66
+ return left[position]! < right[position]! ? 1 : -1;
67
+ }
68
+ }
69
+ if (left.length === right.length) return 0;
70
+ return left.length < right.length ? 1 : -1;
71
+ }
72
+
73
+ function isBetter(left: Candidate, right: Candidate | undefined): boolean {
74
+ if (!right) return true;
75
+ if (left.score !== right.score) return left.score > right.score;
76
+ if (left.cost !== right.cost) return left.cost < right.cost;
77
+ if (left.indices.length !== right.indices.length) return left.indices.length < right.indices.length;
78
+ return compareIndices(left.indices, right.indices) > 0;
79
+ }
80
+
81
+ function validateInput(input: AllocationInput, budgetUnits: number): string[] {
82
+ const errors: string[] = [];
83
+ if (!isFiniteNonnegative(input.budget)) errors.push('invalid-budget');
84
+ if (input.budget > 20000 || budgetUnits > MAX_BUDGET_CENTS) errors.push('budget-too-large');
85
+ input.proposals.forEach((proposal) => {
86
+ if (!proposal.name.trim()) errors.push('missing-name');
87
+ if (!isFiniteNonnegative(proposal.cost)) errors.push('invalid-cost');
88
+ if (!isFiniteNonnegative(proposal.score)) errors.push('invalid-score');
89
+ });
90
+ return [...new Set(errors)];
91
+ }
92
+
93
+ function createEmptyResult(input: AllocationInput, errors: string[]): AllocationResult {
94
+ const allProposals = input.proposals.map((proposal, index) => ({ ...proposal, index, status: 'invalid' as const, reason: 'invalid-input' }));
95
+ return { valid: false, errors, warnings: [], selected: [], excluded: allProposals, allProposals, totals: { budget: input.budget, spent: 0, remaining: input.budget, score: 0, selectedCount: 0 }, tieBreak: TIE_BREAK };
96
+ }
97
+
98
+ function markProposals(input: AllocationInput, selectedIndices: Set<number>, budget: number, spent: number): ProposalResult[] {
99
+ return input.proposals.map((proposal, index) => {
100
+ let status: ProposalStatus = 'not-selected';
101
+ if (selectedIndices.has(index)) status = 'selected';
102
+ if (!selectedIndices.has(index) && proposal.cost > budget) status = 'too-expensive';
103
+ const reason = getProposalReason(status, proposal.cost + spent > budget);
104
+ return { ...proposal, index, status, reason };
105
+ });
106
+ }
107
+
108
+ function getProposalReason(status: ProposalStatus, exceedsRemainingBudget = false): string {
109
+ if (status === 'selected') return 'Included in the best feasible allocation.';
110
+ if (status === 'too-expensive') return 'Its cost exceeds the available budget on its own.';
111
+ if (status === 'invalid') return 'Correct the proposal before running the model.';
112
+ if (exceedsRemainingBudget) return 'Its cost would exceed the remaining budget alongside the selected proposals.';
113
+ return 'A different feasible combination gives a better score under the tie-break rules.';
114
+ }
115
+
116
+ export function allocateBudget(input: AllocationInput): AllocationResult {
117
+ const scale = 10 ** Math.max(decimalPlaces(input.budget), ...input.proposals.map((proposal) => decimalPlaces(proposal.cost)));
118
+ const budgetCents = scaleValue(input.budget, scale);
119
+ const errors = validateInput(input, budgetCents);
120
+ if (errors.length > 0) return createEmptyResult(input, errors);
121
+ const states: Array<Candidate | undefined> = Array.from({ length: budgetCents + 1 }, () => undefined);
122
+ states[0] = { indices: [], cost: 0, score: 0 };
123
+ input.proposals.forEach((proposal, index) => {
124
+ const cost = scaleValue(proposal.cost, scale);
125
+ for (let capacity = budgetCents; capacity >= cost; capacity -= 1) {
126
+ const previous = states[capacity - cost];
127
+ if (!previous) continue;
128
+ const candidate: Candidate = { indices: [...previous.indices, index], cost: previous.cost + cost, score: previous.score + proposal.score };
129
+ if (isBetter(candidate, states[capacity])) states[capacity] = candidate;
130
+ }
131
+ });
132
+ const best = states.reduce<Candidate | undefined>((current, candidate) => isBetter(candidate ?? { indices: [], cost: 0, score: 0 }, current) ? candidate : current, undefined) ?? { indices: [], cost: 0, score: 0 };
133
+ const selectedIndices = new Set(best.indices);
134
+ const spent = best.cost / scale;
135
+ const allProposals = markProposals(input, selectedIndices, input.budget, spent);
136
+ const selected = allProposals.filter((proposal) => proposal.status === 'selected');
137
+ const excluded = allProposals.filter((proposal) => proposal.status !== 'selected');
138
+ const warnings: string[] = [];
139
+ if (input.proposals.length === 0) warnings.push('no-proposals');
140
+ if (input.proposals.length > 0 && best.indices.length === 0) warnings.push('no-feasible-proposal');
141
+ return { valid: true, errors: [], warnings, selected, excluded, allProposals, totals: { budget: input.budget, spent, remaining: input.budget - spent, score: best.score, selectedCount: selected.length }, tieBreak: TIE_BREAK };
142
+ }
143
+
144
+ export function getExampleInput(): AllocationInput {
145
+ return { budget: 12000, proposals: [{ name: 'Pocket park seating', cost: 3500, score: 86 }, { name: 'Safer school crossing', cost: 6200, score: 100 }, { name: 'Community tool library', cost: 4200, score: 74 }, { name: 'Street tree planting', cost: 2800, score: 64 }, { name: 'Public drinking fountain', cost: 1800, score: 38 }] };
146
+ }