From 36675abdef567436b2ef14c59d4be66695bec46f Mon Sep 17 00:00:00 2001 From: BenGlut Date: Fri, 6 Nov 2026 06:29:07 +0100 Subject: [PATCH 1/2] Add the showcase site (GitHub Pages) and a README button that opens it One static page per language (11) built from site/template.html and site/i18n/*.json by site/build.py: how a client connects, the documented integrations with snippets from the guides, the security model, the stability contract, the reference docs and an FAQ. JSON-LD, hreflang, canonical, sharing cards, sitemap with lastmod, robots.txt and llms.txt. site/seo_check.py checks the build (rule: .agents/rules/seo-geo.md); .github/workflows/pages.yml checks it on pull requests and deploys it from main. --- .agents/rules/seo-geo.md | 29 +++ .github/workflows/pages.yml | 90 +++++++++ .gitignore | 3 + README.md | 31 +++ assets/site-button.svg | 35 ++++ llms.txt | 66 +++++++ site/README.md | 47 +++++ site/assets/apple-touch-icon.png | Bin 0 -> 14969 bytes site/assets/favicon.svg | 35 ++++ site/assets/logo-tigertag-black.svg | 15 ++ site/assets/logo-tigertag-white.svg | 20 ++ site/build.py | 248 ++++++++++++++++++++++++ site/i18n/de.json | 125 +++++++++++++ site/i18n/en.json | 125 +++++++++++++ site/i18n/es.json | 125 +++++++++++++ site/i18n/fr.json | 125 +++++++++++++ site/i18n/it.json | 125 +++++++++++++ site/i18n/nl.json | 125 +++++++++++++ site/i18n/pl.json | 125 +++++++++++++ site/i18n/pt-pt.json | 125 +++++++++++++ site/i18n/pt.json | 125 +++++++++++++ site/i18n/ru.json | 125 +++++++++++++ site/i18n/zh.json | 125 +++++++++++++ site/og.jpg | Bin 0 -> 65493 bytes site/seo_check.py | 217 +++++++++++++++++++++ site/site.css | 204 ++++++++++++++++++++ site/site.js | 45 +++++ site/template.html | 281 ++++++++++++++++++++++++++++ 28 files changed, 2741 insertions(+) create mode 100644 .agents/rules/seo-geo.md create mode 100644 .github/workflows/pages.yml create mode 100644 assets/site-button.svg create mode 100644 llms.txt create mode 100644 site/README.md create mode 100644 site/assets/apple-touch-icon.png create mode 100644 site/assets/favicon.svg create mode 100644 site/assets/logo-tigertag-black.svg create mode 100644 site/assets/logo-tigertag-white.svg create mode 100644 site/build.py create mode 100644 site/i18n/de.json create mode 100644 site/i18n/en.json create mode 100644 site/i18n/es.json create mode 100644 site/i18n/fr.json create mode 100644 site/i18n/it.json create mode 100644 site/i18n/nl.json create mode 100644 site/i18n/pl.json create mode 100644 site/i18n/pt-pt.json create mode 100644 site/i18n/pt.json create mode 100644 site/i18n/ru.json create mode 100644 site/i18n/zh.json create mode 100644 site/og.jpg create mode 100644 site/seo_check.py create mode 100644 site/site.css create mode 100644 site/site.js create mode 100644 site/template.html diff --git a/.agents/rules/seo-geo.md b/.agents/rules/seo-geo.md new file mode 100644 index 0000000..0f12b63 --- /dev/null +++ b/.agents/rules/seo-geo.md @@ -0,0 +1,29 @@ +# Search and answer engines (SEO / GEO) + +**Rule.** Everything this repository publishes — the README, `llms.txt` and the showcase +site built from `site/` — is written to be found, understood and quoted correctly by search +engines and AI answer engines, **with verifiable facts only**. The site is checked on every +PR and before every deploy by `site/seo_check.py`; a failing check is fixed, never bypassed. + +**How.** + +1. **Facts, not claims.** Every statement on the site can be checked in this repository (a + doc page, the rules, the changelog). Numbers carry their source or date. No ranking or + "the best" claim. No text or instructions aimed only at crawlers. +2. **Answer first.** A section opens with a sentence that can be quoted alone. Questions + people ask live in a visible FAQ, mirrored by the `FAQPage` JSON-LD. +3. **One complete page per language** (`LANGS` in `site/build.py`): self canonical, + `hreflang` to every language plus `x-default`, `og:locale` with its alternates. +4. **Titles ≤ 60 characters, descriptions 50–160**, unique per language. `seo_check.py` + warns past 60 / 160 and fails past 70 / 200 or on a duplicate across languages. +5. **Structured data mirrors the page**: `Organization`, `WebSite`, `TechArticle` (with + `dateModified`), `FAQPage`, `SoftwareSourceCode` (this repository). One `

` per page. +6. **Images and diagrams** carry a description in the page's language (`alt`, or + `aria-label` on the hero diagram). +7. **Machine-readable copies stay in step.** A fact shown on the site is in the README or + `docs/`, and in `llms.txt`, in the same PR. `llms.txt` is published at the site root. +8. **Same names everywhere**: TigerTag, Tiger Studio Manager, TigerScale, Home Assistant, + Spoolman — spelled as in the README. +9. **No emoji** on the site: inline SVG icons or words. +10. **Checks** before a PR: `python site/build.py --check`, then + `python site/build.py --out _site && python site/seo_check.py _site`. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..2017752 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,90 @@ +name: Publish the showcase site + +# Builds site/ (one static page per language, see site/build.py), checks it +# (site/seo_check.py) and publishes it on GitHub Pages: +# https://tigertag-project.github.io/TigerTag_Firebase_Integration/ +# +# On a pull request the site is only built and checked; it is deployed on a +# push to main. Exactly one workflow deploys this site, and this is it. +# Prerequisite, once: Settings > Pages > Source = "GitHub Actions". + +on: + push: + branches: [main] + paths: + - 'site/**' + - 'llms.txt' + - '.github/workflows/pages.yml' + pull_request: + paths: + - 'site/**' + - 'llms.txt' + - '.github/workflows/pages.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # the page and sitemap dates come from git history + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + + # Every language must have every key before anything is built. + - name: Check the translations + run: python site/build.py --check + + - name: Build the site + run: python site/build.py --out _site + + # Search / answer-engine essentials (.agents/rules/seo-geo.md): no deploy + # with a broken hreflang set, a missing title or invalid JSON-LD. + - name: Check the site for search and answer engines + run: python site/seo_check.py _site + + - if: github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/configure-pages@v5 + - id: deploy + uses: actions/deploy-pages@v4 + + # The published result, not this workflow's green tick: every language answers. + - name: The published site answers in every language + run: | + set -euo pipefail + base="${{ steps.deploy.outputs.page_url }}" + for lang in "" fr/ de/ es/ it/ zh/ pt/ pt-pt/ pl/ ru/ nl/; do + for i in 1 2 3 4 5 6; do + code=$(curl -s -o /dev/null -w '%{http_code}' "${base}${lang}") && [ "$code" = 200 ] && break + sleep 10 + done + echo "${base}${lang} -> ${code}" + [ "$code" = 200 ] + done diff --git a/.gitignore b/.gitignore index b459407..01af794 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ node_modules/ *.local secrets.json service-account*.json + +# Built site (site/build.py) +_site/ diff --git a/README.md b/README.md index 9588ac5..80f3906 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,35 @@ # TigerTag — Firebase Integration Guide +

+ + Visit the integration guide + +

+ +

+ English +  ·  + Français +  ·  + Deutsch +  ·  + Español +  ·  + Italiano +  ·  + 中文 +  ·  + Português (BR) +  ·  + Português (PT) +  ·  + Polski +  ·  + Русский +  ·  + Nederlands +

+ > **🎯 Source of truth for any third-party integration.** If your client > talks to the TigerTag Firebase backend (Home Assistant, OctoPrint, > Spoolman bridge, ESP32 firmware, custom script), **this repo is the @@ -43,6 +73,7 @@ means we shipped a regression. Please file an issue. │ └── python-cli.md ← minimal Python REPL example ├── rules/ │ └── firestore.rules ← public copy of the deployed Security Rules +├── site/ ← the showcase site (GitHub Pages), see site/README.md └── examples/ ├── home-assistant/ ← runnable HA custom component ├── esp32-arduino/ ← reference Arduino sketch diff --git a/assets/site-button.svg b/assets/site-button.svg new file mode 100644 index 0000000..329966a --- /dev/null +++ b/assets/site-button.svg @@ -0,0 +1,35 @@ + + Visit the integration guide + + + + + + + + + + + + + + + + + + + + + + + + + Visit the integration guide + Home Assistant, ESP32, Python, Spoolman + + + diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..22e3a83 --- /dev/null +++ b/llms.txt @@ -0,0 +1,66 @@ +# TigerTag Firebase Integration Guide + +> Reference documentation and examples for third-party clients (Home Assistant, ESP32 firmware, +> Python scripts, a Spoolman bridge, TigerScale-compatible scales) that connect to a user's +> TigerTag account through Firebase Authentication and the Firestore REST API. What is +> documented here is what is deployed; every change to the contract is recorded in CHANGELOG.md. + +Repository: https://github.com/TigerTag-Project/TigerTag_Firebase_Integration +Site: https://tigertag-project.github.io/TigerTag_Firebase_Integration/ +Part of the TigerSystem ecosystem: https://tigersystem.io + +## How a client connects + +1. Fetch the public Firebase config: GET https://tigertag-cdn.web.app/__/firebase/init.json + (the apiKey is public by design; cache the response, a weekly refresh is enough). +2. Sign in with the end user's own email and password: + POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={apiKey} + returns localId (UID), idToken (1-hour JWT) and refreshToken. Google Sign-In needs a browser. +3. Call Firestore with Authorization: Bearer {idToken}. Spools: users/{uid}/inventory, + racks: users/{uid}/racks, friends: users/{uid}/friends. +4. Refresh the idToken before it expires (POST https://securetoken.googleapis.com/v1/token) + and replace both stored tokens; on permission-denied, refresh once and retry once. + +## Principles + +- No service account: every client signs in with the end user's own credentials; never use + the firebase-admin SDK in a third-party client. +- Firestore Security Rules enforce every permission server-side (public copy: rules/firestore.rules). +- Friend access: while users/{friend}/friends/{you} exists, you may read that friend's inventory + and racks, read-only; deleting the document cuts access immediately. +- Firebase App Check is not enforced. +- Polling: 5 minutes minimum for sensors, never under 30 seconds; pass a limit on list reads + (200 for inventory, 50 for racks). + +## Stability + +- Stable without notice: document paths, documented field names and meanings + (weight_available = net grams, last_update = Unix milliseconds, deleted: true = soft-deleted), + the friend access model, the auth endpoints, apiKey and projectId. +- Renaming or removing a field requires at least 3 months notice in the CHANGELOG. +- May change: lookup table contents (ids are append-only), cdn.tigertag.io convenience endpoints, + new collections and fields (always additive). +- Latest released contract version: 0.1.3 (2026-05-02). + +## Docs + +- [Firebase config](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/01-firebase-config.md) +- [Authentication](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/02-authentication.md) +- [Data model](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/03-data-model.md) +- [Friend system](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/04-friend-system.md) +- [Rate limiting](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/05-rate-limiting.md) +- [Firestore Security Rules](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/rules/firestore.rules) +- [Changelog](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/CHANGELOG.md) + +## Client guides + +- [Home Assistant](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/home-assistant.md): one sensor per spool; config flow and sensor skeletons. +- [ESP32 / Arduino](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/esp32.md): find a spool by RFID UID, write weight_available and last_update; complete Arduino sketch. +- [TigerScale contract](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/tigerscale.md): heartbeat every 30 s to users/{uid}/scales/{mac}, weight updates, twin tags in one batch. +- [Python CLI](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/python-cli.md): minimal Pyrebase4 client. +- [Spoolman bridge](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration/blob/main/docs/clients/spoolman-bridge.md): one-way sync TigerTag to Spoolman. + +## Licences + +Documentation: CC-BY-4.0. Security Rules and code snippets: Apache-2.0, attribution waived for +snippets pasted into your own project. The TigerTag name, logo and service are not covered. diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..4e14941 --- /dev/null +++ b/site/README.md @@ -0,0 +1,47 @@ +# The showcase site + +The public page of this integration guide, published on GitHub Pages: + + +It presents the repository — how a client connects, the integrations documented in +`docs/clients/`, the security model, the stability contract and the reference documents — +and links back to them. The repository stays the reference: every fact on the page comes +from the README, `docs/`, `rules/` or `CHANGELOG.md`, and every link points to a file or a +heading of this repository on GitHub. + +It is static HTML, one page per language, built by [`build.py`](build.py) and published by +[`pages.yml`](../.github/workflows/pages.yml) on every push to `main` that touches `site/` +or `llms.txt`. On a pull request the same workflow builds and checks the site without +deploying it. + +## Files + +| File | Contents | +|---|---| +| [`template.html`](template.html) | The page. Every `{{key}}` is replaced by the text of that key in the page's language; `{{@…}}` keys are filled by `build.py` (language menu, `hreflang` links, JSON-LD, repository URL). | +| [`i18n/en.json`](i18n/en.json) | The English text, which is the reference: every other language has exactly its keys. | +| `i18n/.json` | One file per language: `fr`, `de`, `es`, `it`, `zh` (Simplified Chinese), `pt` (Brazil), `pt-pt` (Portugal), `pl`, `ru`, `nl`. | +| [`site.css`](site.css), [`site.js`](site.js) | The look (dark by default, light on request) and the theme / language buttons, the same as the TigerTag protocol site. Loaded with `?v=` and a hash of the file's content. | +| [`seo_check.py`](seo_check.py) | Checks the built pages for search and answer engines (rule: [`.agents/rules/seo-geo.md`](../.agents/rules/seo-geo.md)). | +| [`assets/`](assets/) | TigerTag logos (white on the dark theme, contoured black on the light one), favicon and touch icon, copied unmodified from the TigerTag brand kit. | +| [`og.jpg`](og.jpg) | The picture shown when the site is shared (1200 × 630). | + +The build also publishes [`llms.txt`](../llms.txt) at the site root, a `sitemap.xml` with +`` and a `robots.txt`. + +## Editing + +- **Change a text**: edit `i18n/en.json`, then the same key in every other language. +- **Add a text**: add the key to every `i18n/*.json` file and use `{{key}}` in `template.html`. + The build refuses a language that misses a key, has an unknown one or an empty value. +- **Check and preview**: + + ```bash + python3 site/build.py --check + python3 site/build.py && python3 -m http.server 8000 --directory _site + python3 site/build.py --out _site && python3 site/seo_check.py _site + ``` + +The English page is the root (`/`), every other language has its own folder (`/fr/`, +`/zh/`…). A language chosen in the menu is remembered, and the English root forwards to it +on the next visit. diff --git a/site/assets/apple-touch-icon.png b/site/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7264747a6a41cb5dcc32eb2bf597bedc760ac68a GIT binary patch literal 14969 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 De;Dk~ literal 0 HcmV?d00001 diff --git a/site/assets/favicon.svg b/site/assets/favicon.svg new file mode 100644 index 0000000..b38fe44 --- /dev/null +++ b/site/assets/favicon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/site/assets/logo-tigertag-black.svg b/site/assets/logo-tigertag-black.svg new file mode 100644 index 0000000..42121e8 --- /dev/null +++ b/site/assets/logo-tigertag-black.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/site/assets/logo-tigertag-white.svg b/site/assets/logo-tigertag-white.svg new file mode 100644 index 0000000..6d40c2c --- /dev/null +++ b/site/assets/logo-tigertag-white.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/site/build.py b/site/build.py new file mode 100644 index 0000000..69e69cf --- /dev/null +++ b/site/build.py @@ -0,0 +1,248 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# TigerTag Firebase Integration Guide +# Copyright (c) 2025-2026 TigerTag Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Build the integration guide's showcase site (GitHub Pages) into `_site/`. + +One static page per language, all from `template.html` and `i18n/.json`: +English at the root, the others under `//`. Every page is complete HTML in +its own language — what a search engine, an AI crawler or a reader without +JavaScript sees — with `hreflang` links between them, JSON-LD in its language, +and a sitemap listing them all. + +The site presents the repository; the repository stays the reference. Every link +on the page points to a file or a heading of this repository on GitHub. + +Usage: + python site/build.py # build into _site/ + python site/build.py --check # only check the translations + python site/build.py --out DIR --url https://example.org/path/ +""" + +import argparse +import hashlib +import html +import json +import re +import shutil +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent +SITE_URL = "https://tigertag-project.github.io/TigerTag_Firebase_Integration/" +REPO_URL = "https://github.com/TigerTag-Project/TigerTag_Firebase_Integration" + +# The eleven languages of the TigerTag sites, in the same order. `pt` (Brazil) +# and `pt-pt` (Portugal) are separate dictionaries on purpose. +LANGS = ["en", "fr", "de", "es", "it", "zh", "pt", "pt-pt", "pl", "ru", "nl"] +HTML_LANG = {"zh": "zh-Hans", "pt": "pt-BR", "pt-pt": "pt-PT"} +OG_LOCALE = {"en": "en_US", "fr": "fr_FR", "de": "de_DE", "es": "es_ES", "it": "it_IT", + "zh": "zh_CN", "pt": "pt_BR", "pt-pt": "pt_PT", "pl": "pl_PL", "ru": "ru_RU", "nl": "nl_NL"} + +# Crawlers of search and answer engines, welcomed by name in robots.txt: the +# guide is meant to be read, cited and understood by them. Listing them is an +# explicit opt-in, not a ranking trick. +AI_AGENTS = ["GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-SearchBot", "Claude-User", + "PerplexityBot", "Perplexity-User", "Google-Extended", "Applebot-Extended", "CCBot", + "Amazonbot", "Bytespider", "Meta-ExternalAgent", "cohere-ai", "DuckAssistBot", "MistralAI-User"] + +ORG_ID = "https://tigersystem.io/#organization" +FAQ_COUNT = 7 + +PLACEHOLDER = re.compile(r"\{\{([a-z0-9_@]+)\}\}") + + +def asset_version(name): + """Short sha256 of a published site file, appended to its URL as ?v=.""" + return hashlib.sha256((HERE / name).read_bytes()).hexdigest()[:10] + + +def page_date(): + """The last change to the site or to the README, as an ISO date (git), else today.""" + import subprocess + from datetime import datetime, timezone + try: + date = subprocess.run(["git", "log", "-1", "--format=%cI", "--", "site", "README.md", "llms.txt"], + cwd=REPO, capture_output=True, text=True, check=True).stdout.strip()[:10] + except (OSError, subprocess.CalledProcessError): + date = "" + return date or datetime.now(timezone.utc).date().isoformat() + + +def load_dictionaries(): + """Every language, checked against English: same keys, none empty.""" + dicts = {} + for lang in LANGS: + path = HERE / "i18n" / f"{lang}.json" + if not path.exists(): + raise SystemExit(f"error: missing translation {path.relative_to(REPO)}") + dicts[lang] = json.loads(path.read_text(encoding="utf-8")) + reference = list(dicts["en"]) + problems = [] + for lang, words in dicts.items(): + missing = [k for k in reference if k not in words] + extra = [k for k in words if k not in dicts["en"]] + empty = [k for k, v in words.items() if not isinstance(v, str) or not v.strip()] + for label, keys in (("missing", missing), ("unknown", extra), ("empty", empty)): + if keys: + problems.append(f"{lang}.json: {label} key(s): {', '.join(keys)}") + template_keys = set(PLACEHOLDER.findall((HERE / "template.html").read_text(encoding="utf-8"))) + unused = [k for k in template_keys if not k.startswith("@") and k not in dicts["en"]] + if unused: + problems.append(f"template.html uses unknown key(s): {', '.join(sorted(unused))}") + if problems: + raise SystemExit("error: translations do not match en.json\n " + "\n ".join(problems)) + return dicts + + +def page_path(lang): + return "" if lang == "en" else f"{lang}/" + + +def jsonld_blocks(lang, words, site_url, url, date): + in_lang = HTML_LANG.get(lang, lang) + faq = [{"@type": "Question", "name": words[f"faq{i}_q"], + "acceptedAnswer": {"@type": "Answer", "text": words[f"faq{i}_a"]}} for i in range(1, FAQ_COUNT + 1)] + return [ + {"@context": "https://schema.org", "@type": "Organization", "@id": ORG_ID, "name": "TigerTag Corp", + "url": "https://tigersystem.io", "logo": site_url + "assets/apple-touch-icon.png", + "sameAs": ["https://github.com/TigerTag-Project", + "https://tigertag-project.github.io/TigerTag-RFID-Guide/"]}, + {"@context": "https://schema.org", "@type": "WebSite", "name": "TigerTag Firebase Integration", "url": url, + "inLanguage": in_lang, "description": words["meta_description"], "publisher": {"@id": ORG_ID}}, + {"@context": "https://schema.org", "@type": "TechArticle", "headline": words["meta_title"], + "description": words["meta_description"], + "about": "Connecting third-party clients to a TigerTag account through Firebase Authentication and Firestore", + "inLanguage": in_lang, "url": url, "image": site_url + "assets/og.jpg", "dateModified": date, + "license": "https://creativecommons.org/licenses/by/4.0/", + "isBasedOn": REPO_URL, "author": {"@id": ORG_ID}, "publisher": {"@id": ORG_ID}}, + {"@context": "https://schema.org", "@type": "FAQPage", "inLanguage": in_lang, "mainEntity": faq}, + {"@context": "https://schema.org", "@type": "SoftwareSourceCode", + "name": "TigerTag Firebase Integration Guide", "codeRepository": REPO_URL, + "programmingLanguage": ["Python", "C++"], + "license": "https://www.apache.org/licenses/LICENSE-2.0", + "description": "Reference documentation and examples for clients (Home Assistant, ESP32, Python, Spoolman bridge) " + "that sign in with the user's own TigerTag account and read or write Firestore.", + "author": {"@id": ORG_ID}}, + ] + + +def render(template, lang, words, dicts, site_url, date): + base = "" if lang == "en" else "../" + url = site_url + page_path(lang) + + alternates = [f'' for l in LANGS] + alternates.append(f'') + + menu = [] + for l in LANGS: + current = ' aria-current="true"' if l == lang else "" + menu.append(f'
  • {html.escape(dicts[l]["lang_name"])}
  • ') + + # ` block. + jsonld_html = "\n".join('" + for j in jsonld_blocks(lang, words, site_url, url, date)) + + special = { + "@lang": lang, + "@htmllang": HTML_LANG.get(lang, lang), + "@oglocale": OG_LOCALE[lang], + "@oglocalealt": "\n".join(f'' for l in LANGS if l != lang), + "@url": url, + "@siteurl": site_url, + "@base": base, + "@langpath": page_path(lang), + "@alternates": "\n".join(alternates), + "@langmenu": "\n".join(menu), + "@jsonld": jsonld_html, + "@repo": REPO_URL, + # Content hashes: a new site.css / site.js gets a new URL, so no browser + # keeps running the previous one from its cache. + "@vcss": asset_version("site.css"), + "@vjs": asset_version("site.js"), + "@trademark": f'{html.escape(words["foot_trademark"])}', + } + + def fill(match): + key = match.group(1) + if key in special: + return special[key] + return html.escape(words[key], quote=True) + + return PLACEHOLDER.sub(fill, template) + + +def build(out, site_url): + dicts = load_dictionaries() + template = (HERE / "template.html").read_text(encoding="utf-8") + date = page_date() + if out.exists(): + shutil.rmtree(out) + (out / "assets").mkdir(parents=True) + + for lang in LANGS: + target = out / page_path(lang) / "index.html" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(render(template, lang, dicts[lang], dicts, site_url, date), encoding="utf-8") + + for name in ("site.css", "site.js"): + shutil.copy2(HERE / name, out / name) + # Logos (white on dark, contoured black on light), favicon and touch icon. + for extra in sorted((HERE / "assets").glob("*")): + shutil.copy2(extra, out / "assets" / extra.name) + shutil.copy2(HERE / "og.jpg", out / "assets" / "og.jpg") + # The machine-readable description of the guide, at the site root too. + shutil.copy2(REPO / "llms.txt", out / "llms.txt") + + urls = "\n".join( + " {}{}{}".format( + site_url + page_path(lang), date, + "".join(f'' for l in LANGS)) + for lang in LANGS) + (out / "sitemap.xml").write_text( + '\n' + '\n' + f"{urls}\n\n", encoding="utf-8") + robots = ["User-agent: *", "Allow: /", ""] + for agent in AI_AGENTS: + robots += [f"User-agent: {agent}", "Allow: /", ""] + robots += [f"Sitemap: {site_url}sitemap.xml"] + (out / "robots.txt").write_text("\n".join(robots) + "\n", encoding="utf-8") + (out / ".nojekyll").write_text("", encoding="utf-8") + print(f"built {len(LANGS)} languages into {out}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--out", default=str(REPO / "_site")) + parser.add_argument("--url", default=SITE_URL) + parser.add_argument("--check", action="store_true") + parser.add_argument("--only", help="comma-separated languages, for a quick local preview") + args = parser.parse_args() + if args.only: + LANGS[:] = [l for l in LANGS if l in args.only.split(",")] + if args.check: + load_dictionaries() + print(f"translations OK: {len(LANGS)} languages") + return + url = args.url if args.url.endswith("/") else args.url + "/" + build(Path(args.out), url) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/site/i18n/de.json b/site/i18n/de.json new file mode 100644 index 0000000..01ed247 --- /dev/null +++ b/site/i18n/de.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Deutsch", + "skip": "Zum Inhalt springen", + "theme_label": "Hell oder dunkel", + "lang_label": "Sprache wählen", + "meta_title": "TigerTag Firebase-Integration: eigenen Client anbinden", + "meta_description": "Referenz für Home-Assistant-, ESP32-, Python- und Spoolman-Clients, die TigerTag-Filamentspulen über Firebase Auth und Firestore lesen und aktualisieren.", + "og_title": "Leitfaden zur TigerTag Firebase-Integration", + "og_description": "Mit dem eigenen TigerTag-Konto des Nutzers anmelden, seine Spulen lesen, Gewichte aktualisieren: die Referenz für Drittanbieter-Clients, mit Beispielen.", + "og_alt": "TigerTag Firebase-Integration: die Referenz für Home-Assistant-, ESP32-, Python- und Spoolman-Clients", + "nav_how": "So funktioniert es", + "nav_integrations": "Integrationen", + "nav_security": "Sicherheit", + "nav_contract": "Vertrag", + "nav_docs": "Doku", + "nav_faq": "FAQ", + "hero_kicker": "Referenz für Drittanbieter-Integrationen", + "hero_headline": "Verbinden Sie Home Assistant, eine ESP32-Waage oder ein Python-Skript mit einem TigerTag-Konto.", + "hero_lead": "Dieses Repository beschreibt, wie sich ein beliebiger Client mit den eigenen TigerTag-Zugangsdaten des Nutzers anmeldet, dessen Filamentspulen und Racks in Firestore liest und Spulengewichte zurückschreibt. Was hier dokumentiert ist, ist das, was bereitgestellt ist, und jede Änderung daran wird im Changelog festgehalten.", + "cta_start": "Integration starten", + "cta_examples": "Beispiele ansehen", + "cta_changelog": "Changelog lesen", + "flow_alt": "So verbindet sich ein Client: Der Client meldet sich mit E-Mail und Passwort des Nutzers bei Firebase Authentication an, sendet das erhaltene idToken an Firestore, und die Firestore Security Rules entscheiden, was er in users/{uid}/inventory lesen oder schreiben darf.", + "flow_client": "Ihr Client", + "flow_auth": "Das eigene Konto des Nutzers, ein idToken für 1 Stunde", + "flow_rules": "Eigentümer lesen und schreiben, Freunde lesen", + "fact1_value": "0.1.3", + "fact1_label": "Neueste veröffentlichte Vertragsversion (CHANGELOG, 2. Mai 2026)", + "fact2_value": "1 h", + "fact2_label": "Gültigkeitsdauer eines idToken: vor Ablauf erneuern", + "fact3_value": "5 min", + "fact3_label": "Empfohlenes minimales Abfrageintervall", + "fact4_value": "3 Monate", + "fact4_label": "Mindestvorlauf für eine Abkündigung, bevor ein Feld umbenannt oder entfernt wird", + "how_title": "So verbindet sich ein Client mit TigerTag", + "how_lead": "Jeder Client folgt denselben vier Schritten: die öffentliche Firebase-Konfiguration abrufen, sich als Nutzer anmelden, die Firestore-REST-API mit dem Token des Nutzers aufrufen und dieses Token aktuell halten.", + "step1_title": "Öffentliche Konfiguration abrufen", + "step1_text": "Ein einziger GET liefert die Konfiguration des Firebase-Projekts. Der apiKey ist absichtlich öffentlich: Er identifiziert das Projekt, nicht den Aufrufer. Speichern Sie ihn zwischen; eine wöchentliche Aktualisierung genügt.", + "step2_title": "Als Nutzer anmelden", + "step2_text": "E-Mail und Passwort des Nutzers liefern eine UID, ein idToken für 1 Stunde und ein refreshToken. Google Sign-In benötigt einen Browser, daher verwenden Clients ohne Oberfläche ein Passwort.", + "step3_title": "Firestore lesen und schreiben", + "step3_text": "Senden Sie das idToken als Bearer-Token. Spulen liegen in users/{uid}/inventory, Racks in users/{uid}/racks; Gewichte sind Nettogramm, Datumsangaben Unix-Millisekunden.", + "step4_title": "Token aktuell halten", + "step4_text": "Erneuern Sie das idToken, bevor die Stunde abläuft, und ersetzen Sie beide gespeicherten Tokens. Bei permission-denied einmal erneuern und den Lesevorgang einmal wiederholen.", + "step4_link": "idToken erneuern", + "how_code_title": "Die drei Aufrufe aus der Dokumentation", + "how_code_note": "Die Werte in geschweiften Klammern stammen aus dem vorherigen Aufruf: apiKey und projectId aus der Konfiguration, uid und idToken aus der Anmeldung.", + "int_title": "In diesem Repository dokumentierte Integrationen", + "int_lead": "Jeder Leitfaden in docs/clients behandelt die Authentifizierung, die gelesenen oder geschriebenen Daten, die Feldzuordnung und den Code. Die folgenden Ausschnitte sind aus den Leitfäden übernommen.", + "ha_pill": "Komponentenentwurf", + "ha_text": "Eine benutzerdefinierte Home-Assistant-Komponente, die pro Spule einen Sensor bereitstellt, mit dem Restgewicht in Gramm als Zustand. Der Leitfaden behandelt auch die Inventare von Freunden, Paginierung und Token-Speicherung; Config Flow und Sensor sind Gerüste, eine vollständig lauffähige Komponente muss noch geschrieben werden.", + "ha_sketch": "Komponentenentwurf (Abschnitt 9)", + "esp_pill": "Arduino-Sketch", + "esp_text": "Für eine Waage oder ein NFC-Gerät: eine Spule anhand der gescannten RFID-UID suchen (uid, dann twin_uid), das Nettogewicht berechnen und weight_available und last_update mit einer Update-Maske schreiben. Der Leitfaden endet mit einem vollständigen ESP32-Sketch (WiFi, ArduinoJson).", + "py_pill": "Vollständiger Client", + "py_text": "Ein minimaler Python-Client auf Basis von Pyrebase4: Er meldet sich an, listet Ihre Spulen auf und zeigt, wie viele Spulen jeder Freund hat. Der Leitfaden enthält das gesamte Skript und einen Beispiellauf.", + "sm_pill": "Vollständiges Skript", + "sm_text": "Eine Einweg-Brücke von TigerTag zu einer Spoolman-Instanz: Marken werden zu Herstellern, jede Kombination aus Marke, Material und Farbe wird zu einem Filament, und jedes Inventardokument wird zu einer Spule, die über die REST-API von Spoolman angelegt oder aktualisiert wird. Ausführbar per cron, systemd oder Docker.", + "scale_pill": "Gerätevertrag", + "scale_text": "Der Vertrag, dem eine TigerScale oder kompatible Waage folgt, um in Tiger Studio Manager und der mobilen TigerTag-App online zu erscheinen: alle 30 Sekunden ein Heartbeat-Dokument, bei jeder stabilen Messung eine Gewichtsaktualisierung und Zwillings-Tags, die in einem einzigen atomaren Batch geschrieben werden.", + "read_guide": "Leitfaden lesen", + "example_folder": "Beispielordner", + "int_note": "Sie entwickeln für eine andere Plattform? Ein neuer Leitfaden gehört nach docs/clients, ein optionales lauffähiges Beispiel nach examples/.", + "sec_title": "Das Konto des Nutzers, die Regeln des Nutzers", + "sec_lead": "Es gibt keinen gemeinsamen Schlüssel zu schützen und kein Master-Token, das durchsickern könnte: Ein Client kann genau das tun, was sein Nutzer tun kann, und Firestore setzt das auf dem Server durch.", + "sec1_title": "Kein Dienstkonto", + "sec1_text": "Jeder Client meldet sich mit den eigenen Zugangsdaten des Endnutzers an. Liefern Sie niemals das firebase-admin SDK oder einen Admin-Schlüssel in einem Drittanbieter-Client aus.", + "sec2_title": "Die Security Rules entscheiden", + "sec2_text": "Die Firestore Security Rules prüfen jede Anfrage anhand von request.auth.uid: Eigentümer lesen und schreiben ihre eigenen Dokumente, Freunde dürfen Inventar und Racks lesen, und alles nicht Erlaubte wird abgelehnt. Die öffentliche Kopie der Regeln liegt in rules/firestore.rules.", + "sec3_title": "Eine Verbindung pro Freundschaft", + "sec3_text": "Wird eine Freundschaftsanfrage angenommen, entsteht auf jeder Seite ein Dokument. Solange users/{friend}/friends/{you} existiert, dürfen Sie Inventar und Racks dieses Freundes lesen, nur lesend; das Löschen beendet den Zugriff sofort.", + "sec4_title": "Rücksichtsvolles Abfragen", + "sec4_text": "Fragen Sie höchstens alle 5 Minuten ab, nie in kürzeren Abständen als 30 Sekunden, schreiben Sie nur, wenn sich ein Wert ändert, und geben Sie bei Listenabfragen ein Limit an (200 Spulen, 50 Racks). Firebase App Check wird nicht erzwungen.", + "contract_title": "Ein öffentlicher Vertrag für Integratoren", + "contract_lead": "Das Repository wird als öffentlicher Vertrag behandelt: Änderungen, die Clients betreffen, erscheinen in diesem Repository mit demselben Release, und das Changelog verzeichnet jede einzelne als Added, Changed, Deprecated, Removed, Fixed oder Security.", + "stable_title": "Ändert sich nicht ohne Ankündigung", + "stable1": "Dokumentpfade wie users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} und users/{uid}/friends/{friendUid}.", + "stable2": "Name und Bedeutung jedes dokumentierten Feldes: weight_available ist Nettogramm, last_update Unix-Millisekunden, deleted: true bedeutet vorläufig gelöscht (Soft Delete).", + "stable3": "Das Zugriffsmodell für Freunde und die Firebase-Auth-Endpunkte, über die sich ein Client anmeldet.", + "stable4": "Der apiKey und die projectId, es sei denn, das gesamte Projekt zieht um, was mindestens 6 Monate im Voraus angekündigt wird.", + "may_title": "Kann sich ändern: defensiv programmieren", + "may1": "Nachschlagetabellen erhalten neue Marken und Materialien; numerische IDs werden nur angehängt und nie wiederverwendet.", + "may2": "Komfort-HTTPS-Endpunkte unter cdn.tigertag.io können die Form ihrer Anfragen und Antworten weiterentwickeln.", + "may3": "Neue Collections und neue Felder kommen immer nur hinzu: Ignorieren Sie unbekannte Felder und bleiben Sie kompatibel.", + "contract_link": "Stabilitätsgarantien im README", + "docs_title": "Referenzdokumentation", + "docs_lead": "Die Kerndokumente, auf denen jede Integration aufbaut, in der Reihenfolge, in der ein Client sie braucht.", + "doc1": "Der öffentliche Firebase-Konfigurationsendpunkt, wozu jedes Feld dient und wie lange er zwischengespeichert wird.", + "doc2": "Anmeldung mit E-Mail und Passwort, Token-Erneuerung und das Wiederholungsmuster bei permission-denied.", + "doc3": "Jede Firestore-Collection und jedes Feld: Inventar, Racks, Freunde, Profile, Waagen.", + "doc4": "Wie der Zugriff für Freunde funktioniert: öffentliche Codes, private Schlüssel, die beidseitige Verbindung.", + "doc5": "Abfrageintervalle, Abfragelimits, Überwachung und warum App Check nicht erzwungen wird.", + "doc6": "Die öffentliche Kopie der bereitgestellten Firestore Security Rules.", + "eco_title": "Teil des TigerTag-Ökosystems", + "eco_lead": "Dieser Leitfaden behandelt das Cloud-Konto. Die Chips, die Apps und die SDKs haben eigene Repositorys.", + "eco_proto_title": "Das TigerTag-Protokoll", + "eco_proto_text": "Das offene NFC-Chipformat für Filamentspulen und Harzflaschen, seine Spezifikation und die Referenzdatenbank.", + "eco_studio_text": "Die Desktop-App (Electron), die dasselbe Inventar verwaltet; die Dokumentation wird anhand ihrer Releases geprüft.", + "eco_sdk_title": "TigerTag-SDKs", + "eco_sdk_text": "TigerTag-Chips offline lesen, prüfen, erstellen und patchen, in JavaScript oder Python, unter Apache-2.0.", + "faq_title": "Fragen", + "faq1_q": "Brauche ich ein Dienstkonto oder einen Admin-Schlüssel?", + "faq1_a": "Nein. Jeder Client meldet sich mit der eigenen TigerTag-E-Mail und dem Passwort des Endnutzers an, und die Firestore Security Rules entscheiden, was dieser Nutzer lesen oder schreiben darf. Es gibt keinen gemeinsamen Admin-Schlüssel, und das firebase-admin SDK darf niemals in einem Drittanbieter-Client verwendet werden.", + "faq2_q": "Warum ist der Firebase-apiKey öffentlich?", + "faq2_a": "Der apiKey identifiziert das TigerTag-Firebase-Projekt, nicht den Aufrufer. Ohne gültiges idToken, das man durch Anmeldung als echter Nutzer erhält, gewährt er keinerlei Zugriff; die Zugriffskontrolle erfolgt über die Authentifizierung und die Security Rules.", + "faq3_q": "Können sich Home Assistant oder ein ESP32 mit Google anmelden?", + "faq3_a": "Nicht direkt: Google Sign-In erfordert eine Browser-Weiterleitung. Ein Nutzer, der bisher nur Google verwendet hat, legt zuerst über die Passwort-vergessen-Funktion der Desktop- oder Mobil-App ein Passwort für das Konto fest und meldet sich dann mit E-Mail und Passwort an.", + "faq4_q": "Wie oft darf meine Integration abfragen?", + "faq4_a": "Höchstens alle 5 Minuten für einen Sensor, alle 15 Minuten für eine Rack-Übersicht und nie in kürzeren Abständen als 30 Sekunden. Eine Waage schreibt nur, wenn sich ein Gewicht ändert. Listenabfragen sollten ein Limit angeben: 200 für das Inventar, 50 für Racks.", + "faq5_q": "Kann meine Integration die Spulen eines Freundes lesen?", + "faq5_a": "Ja, nur lesend. Sind zwei Nutzer befreundet, hat jeder unter users/{uid}/friends ein Dokument für den anderen, und die Regeln erlauben einem Freund, Inventar und Racks zu lesen. Jeder der beiden kann die Freundschaft jederzeit beenden, was den Zugriff sofort aufhebt.", + "faq6_q": "Was, wenn die Produktion von dieser Dokumentation abweicht?", + "faq6_a": "Die Dokumentation gilt: Eine Abweichung bedeutet, dass eine Regression ausgeliefert wurde. Eröffnen Sie ein Issue im Repository mit der Seite, dem gelesenen oder geschriebenen Pfad und der Antwort, ohne Tokens und private Schlüssel.", + "faq7_q": "Unter welcher Lizenz darf ich den Code wiederverwenden?", + "faq7_a": "Die Security Rules und alle Code-Ausschnitte in der Dokumentation stehen unter Apache-2.0, und für Ausschnitte, die Sie in Ihr eigenes Projekt einfügen, wird auf die Namensnennung verzichtet. Die Dokumentation selbst steht unter CC-BY-4.0. Name, Logo und Dienst von TigerTag fallen nicht unter diese Lizenzen.", + "join_title": "Starten Sie Ihre Integration", + "join_text": "Wählen Sie in den Schnelllinks des README den Leitfaden für Ihre Plattform und folgen Sie ihm von der Anmeldung bis zum ersten Lesezugriff.", + "join_btn": "Schnelllinks öffnen", + "foot_licences": "Dokumentation unter CC-BY-4.0, Code und Ausschnitte unter Apache-2.0. Der TigerTag-Dienst wird von TigerTag Corp betrieben und fällt nicht unter diese Lizenzen.", + "foot_trademark": "TigerTag ist eine Marke der TigerTag Corp. Markenrichtlinie", + "foot_source": "Quelltext dieser Seite", + "foot_issues": "Problem in der Dokumentation melden", + "repo": "Repository", + "open": "Öffnen" +} diff --git a/site/i18n/en.json b/site/i18n/en.json new file mode 100644 index 0000000..851ff36 --- /dev/null +++ b/site/i18n/en.json @@ -0,0 +1,125 @@ +{ + "lang_name": "English", + "skip": "Skip to content", + "theme_label": "Light or dark", + "lang_label": "Choose a language", + "meta_title": "TigerTag Firebase Integration: connect your own client", + "meta_description": "Reference for Home Assistant, ESP32, Python and Spoolman clients that read and update TigerTag filament spools through Firebase Auth and Firestore.", + "og_title": "TigerTag Firebase Integration Guide", + "og_description": "Sign in with the user's own TigerTag account, read their spools, update weights: the reference for third-party clients, with examples.", + "og_alt": "TigerTag Firebase Integration: the reference for Home Assistant, ESP32, Python and Spoolman clients", + "nav_how": "How it connects", + "nav_integrations": "Integrations", + "nav_security": "Security", + "nav_contract": "Contract", + "nav_docs": "Docs", + "nav_faq": "FAQ", + "hero_kicker": "Third-party integration reference", + "hero_headline": "Connect Home Assistant, an ESP32 scale or a Python script to a TigerTag account.", + "hero_lead": "This repository documents how any client signs in with the user's own TigerTag credentials, reads their filament spools and racks in Firestore, and writes spool weights back. What is documented here is what is deployed, and every change to it is recorded in the changelog.", + "cta_start": "Start an integration", + "cta_examples": "Browse the examples", + "cta_changelog": "Read the changelog", + "flow_alt": "How a client connects: the client signs in to Firebase Authentication with the user's email and password, sends the returned idToken to Firestore, and Firestore Security Rules decide what it may read or write in users/{uid}/inventory.", + "flow_client": "Your client", + "flow_auth": "The user's own account, a 1-hour idToken", + "flow_rules": "Owners read and write, friends read", + "fact1_value": "0.1.3", + "fact1_label": "Latest released contract version (CHANGELOG, 2 May 2026)", + "fact2_value": "1 h", + "fact2_label": "Lifetime of an idToken: refresh it before it expires", + "fact3_value": "5 min", + "fact3_label": "Recommended minimum polling interval", + "fact4_value": "3 months", + "fact4_label": "Minimum deprecation notice before a field is renamed or removed", + "how_title": "How a client connects to TigerTag", + "how_lead": "Every client follows the same four steps: fetch the public Firebase config, sign in as the user, call the Firestore REST API with the user's token, and keep that token fresh.", + "step1_title": "Fetch the public config", + "step1_text": "One GET returns the Firebase project config. The apiKey is public by design: it identifies the project, not the caller. Cache it; a weekly refresh is enough.", + "step2_title": "Sign in as the user", + "step2_text": "The user's own email and password return a UID, a 1-hour idToken and a refreshToken. Google Sign-In needs a browser, so headless clients use a password.", + "step3_title": "Read and write Firestore", + "step3_text": "Send the idToken as a Bearer token. Spools live in users/{uid}/inventory, racks in users/{uid}/racks; weights are net grams, dates Unix milliseconds.", + "step4_title": "Keep the token fresh", + "step4_text": "Refresh the idToken before the hour is up and replace both stored tokens. On a permission-denied, refresh once and retry the read once.", + "step4_link": "Refreshing the idToken", + "how_code_title": "The three calls, from the documentation", + "how_code_note": "The values in braces come from the previous call: apiKey and projectId from the config, uid and idToken from the sign-in.", + "int_title": "Integrations documented in this repository", + "int_lead": "Each guide in docs/clients covers authentication, the data it reads or writes, the field mapping and the code. The snippets below are copied from the guides.", + "ha_pill": "Component sketch", + "ha_text": "A Home Assistant custom component that exposes one sensor per spool, with the remaining weight in grams as its state. The guide also covers friends' inventories, pagination and token storage; the config flow and sensor are skeletons, and a fully runnable component is still to be written.", + "ha_sketch": "Component sketch (section 9)", + "esp_pill": "Arduino sketch", + "esp_text": "For a scale or NFC device: look up a spool by the scanned RFID UID (uid, then twin_uid), compute the net weight and write weight_available and last_update with an update mask. The guide ends with a complete ESP32 sketch (WiFi, ArduinoJson).", + "py_pill": "Complete client", + "py_text": "A minimal Python client built on Pyrebase4: it signs in, lists your spools and prints how many spools each friend has. The guide holds the whole script and a sample run.", + "sm_pill": "Complete script", + "sm_text": "A one-way bridge from TigerTag to a Spoolman instance: brands become vendors, each brand, material and colour becomes a filament, and each inventory document becomes a spool, upserted through Spoolman's REST API. Run it from cron, systemd or Docker.", + "scale_pill": "Device contract", + "scale_text": "The contract a TigerScale or compatible scale follows to appear online in Tiger Studio Manager and the TigerTag mobile app: a heartbeat document every 30 seconds, a weight update on every stable reading, and twin tags written in one atomic batch.", + "read_guide": "Read the guide", + "example_folder": "Example folder", + "int_note": "Building for another platform? A new guide goes in docs/clients, an optional runnable example in examples/.", + "sec_title": "The user's account, the user's rules", + "sec_lead": "There is no shared key to protect and no master token to leak: a client can do exactly what its user can do, and Firestore enforces it on the server.", + "sec1_title": "No service account", + "sec1_text": "Every client signs in with the end user's own credentials. Never ship the firebase-admin SDK or any admin key in a third-party client.", + "sec2_title": "Security Rules decide", + "sec2_text": "Firestore Security Rules check every request against request.auth.uid: owners read and write their own documents, friends may read inventory and racks, and anything not allowed is denied. The public copy of the rules is in rules/firestore.rules.", + "sec3_title": "One bond per friendship", + "sec3_text": "Accepting a friend request writes a document on each side. While users/{friend}/friends/{you} exists you may read that friend's inventory and racks, read-only; deleting it cuts access immediately.", + "sec4_title": "Polite polling", + "sec4_text": "Poll every 5 minutes at most often, never under 30 seconds, write only when a value changes, and pass a limit on list reads (200 spools, 50 racks). Firebase App Check is not enforced.", + "contract_title": "A public contract for integrators", + "contract_lead": "The repository is treated as a public contract: changes that affect clients reach this repository in the same release, and the changelog records each one as Added, Changed, Deprecated, Removed, Fixed or Security.", + "stable_title": "Will not change without notice", + "stable1": "Document paths such as users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} and users/{uid}/friends/{friendUid}.", + "stable2": "The name and meaning of every documented field: weight_available is net grams, last_update is Unix milliseconds, deleted: true means soft-deleted.", + "stable3": "The friend access model, and the Firebase Auth endpoints a client signs in with.", + "stable4": "The apiKey and projectId, unless the whole project moves, announced at least 6 months ahead.", + "may_title": "May change: be defensive", + "may1": "Lookup tables gain new brands and materials; numeric ids are append-only and never reused.", + "may2": "Convenience HTTPS endpoints at cdn.tigertag.io can evolve their request and response shapes.", + "may3": "New collections and new fields are always additive: ignore the fields you do not know and stay compatible.", + "contract_link": "Stability guarantees in the README", + "docs_title": "Reference documentation", + "docs_lead": "The core documents every integration builds on, in the order a client needs them.", + "doc1": "The public Firebase config endpoint, what each field is for, and how long to cache it.", + "doc2": "Email and password sign-in, token refresh, and the permission-denied retry pattern.", + "doc3": "Every Firestore collection and field: inventory, racks, friends, profiles, scales.", + "doc4": "How friend access works: public codes, private keys, the bidirectional bond.", + "doc5": "Polling intervals, query limits, monitoring, and why App Check is not enforced.", + "doc6": "The public copy of the deployed Firestore Security Rules.", + "eco_title": "Part of the TigerTag ecosystem", + "eco_lead": "This guide covers the cloud account. The chips, the apps and the SDKs have their own repositories.", + "eco_proto_title": "The TigerTag protocol", + "eco_proto_text": "The open NFC chip format for filament spools and resin bottles, its specification and the reference database.", + "eco_studio_text": "The desktop app (Electron) that manages the same inventory; the documentation is verified against its releases.", + "eco_sdk_title": "TigerTag SDKs", + "eco_sdk_text": "Read, verify, create and patch TigerTag chips offline, in JavaScript or Python, under Apache-2.0.", + "faq_title": "Questions", + "faq1_q": "Do I need a service account or an admin key?", + "faq1_a": "No. Every client signs in with the end user's own TigerTag email and password, and Firestore Security Rules decide what that user may read or write. There is no shared admin key, and the firebase-admin SDK must never be used in a third-party client.", + "faq2_q": "Why is the Firebase apiKey public?", + "faq2_a": "The apiKey identifies the TigerTag Firebase project, not the caller. Without a valid idToken, obtained by signing in as a real user, it gives access to nothing; access control is done by authentication and by the Security Rules.", + "faq3_q": "Can Home Assistant or an ESP32 sign in with Google?", + "faq3_a": "Not directly: Google Sign-In needs a browser redirect. A user who has only ever used Google sets a password on the account first, through the desktop or mobile app's forgot-password flow, and then signs in with email and password.", + "faq4_q": "How often may my integration poll?", + "faq4_a": "Every 5 minutes at most often for a sensor, every 15 minutes for a rack overview, and never under 30 seconds. A scale writes only when a weight changes. List reads should pass a limit: 200 for inventory, 50 for racks.", + "faq5_q": "Can my integration read a friend's spools?", + "faq5_a": "Yes, read-only. When two users are friends, each has a document for the other under users/{uid}/friends, and the rules let a friend read the inventory and racks. Either user can remove the friendship at any time, which cuts access immediately.", + "faq6_q": "What if production disagrees with this documentation?", + "faq6_a": "The documentation wins: a difference means a regression shipped. Open an issue on the repository with the page, the path you read or wrote and the response, with tokens and private keys removed.", + "faq7_q": "Under which licence can I reuse the code?", + "faq7_a": "The Security Rules and every code snippet in the documentation are under Apache-2.0, and attribution is waived for snippets pasted into your own project. The documentation itself is under CC-BY-4.0. The TigerTag name, logo and service are not covered by these licences.", + "join_title": "Start your integration", + "join_text": "Pick the guide for your platform in the README's quick links, and follow it from sign-in to your first read.", + "join_btn": "Open the quick links", + "foot_licences": "Documentation under CC-BY-4.0, code and snippets under Apache-2.0. The TigerTag service is operated by TigerTag Corp and is not covered by these licences.", + "foot_trademark": "TigerTag is a trademark of TigerTag Corp. Trademark policy", + "foot_source": "Source of this page", + "foot_issues": "Report a documentation issue", + "repo": "Repository", + "open": "Open" +} diff --git a/site/i18n/es.json b/site/i18n/es.json new file mode 100644 index 0000000..b969e77 --- /dev/null +++ b/site/i18n/es.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Español", + "skip": "Ir al contenido", + "theme_label": "Claro u oscuro", + "lang_label": "Elegir idioma", + "meta_title": "Integración Firebase de TigerTag: conecta tu propio cliente", + "meta_description": "Referencia para clientes de Home Assistant, ESP32, Python y Spoolman que leen y actualizan bobinas de filamento TigerTag mediante Firebase Auth y Firestore.", + "og_title": "Guía de integración de TigerTag con Firebase", + "og_description": "Inicia sesión con la cuenta TigerTag del propio usuario, lee sus bobinas y actualiza los pesos: la referencia para clientes de terceros, con ejemplos.", + "og_alt": "Integración Firebase de TigerTag: la referencia para clientes de Home Assistant, ESP32, Python y Spoolman", + "nav_how": "Cómo se conecta", + "nav_integrations": "Integraciones", + "nav_security": "Seguridad", + "nav_contract": "Contrato", + "nav_docs": "Documentación", + "nav_faq": "Preguntas frecuentes", + "hero_kicker": "Referencia de integración para terceros", + "hero_headline": "Conecta Home Assistant, una báscula ESP32 o un script de Python a una cuenta TigerTag.", + "hero_lead": "Este repositorio documenta cómo cualquier cliente inicia sesión con las credenciales TigerTag del propio usuario, lee sus bobinas de filamento y sus estanterías en Firestore y vuelve a escribir el peso de las bobinas. Lo que se documenta aquí es lo que está desplegado, y cada cambio queda registrado en el changelog.", + "cta_start": "Empezar una integración", + "cta_examples": "Ver los ejemplos", + "cta_changelog": "Leer el changelog", + "flow_alt": "Cómo se conecta un cliente: el cliente inicia sesión en Firebase Authentication con el correo y la contraseña del usuario, envía el idToken devuelto a Firestore, y las Firestore Security Rules deciden qué puede leer o escribir en users/{uid}/inventory.", + "flow_client": "Tu cliente", + "flow_auth": "La cuenta del propio usuario, un idToken de 1 hora", + "flow_rules": "Los propietarios leen y escriben, los amigos leen", + "fact1_value": "0.1.3", + "fact1_label": "Última versión publicada del contrato (CHANGELOG, 2 de mayo de 2026)", + "fact2_value": "1 h", + "fact2_label": "Duración de un idToken: renuévalo antes de que caduque", + "fact3_value": "5 min", + "fact3_label": "Intervalo mínimo de sondeo recomendado", + "fact4_value": "3 meses", + "fact4_label": "Preaviso mínimo de obsolescencia antes de renombrar o eliminar un campo", + "how_title": "Cómo se conecta un cliente a TigerTag", + "how_lead": "Todos los clientes siguen los mismos cuatro pasos: obtener la configuración pública de Firebase, iniciar sesión como el usuario, llamar a la API REST de Firestore con el token del usuario y mantener ese token vigente.", + "step1_title": "Obtener la configuración pública", + "step1_text": "Un solo GET devuelve la configuración del proyecto Firebase. El apiKey es público por diseño: identifica el proyecto, no a quien llama. Guárdalo en caché; basta con renovarlo una vez por semana.", + "step2_title": "Iniciar sesión como el usuario", + "step2_text": "El correo y la contraseña del propio usuario devuelven un UID, un idToken de 1 hora y un refreshToken. Google Sign-In necesita un navegador, así que los clientes sin interfaz usan una contraseña.", + "step3_title": "Leer y escribir en Firestore", + "step3_text": "Envía el idToken como token Bearer. Las bobinas están en users/{uid}/inventory y las estanterías en users/{uid}/racks; los pesos son gramos netos y las fechas, milisegundos Unix.", + "step4_title": "Mantener el token vigente", + "step4_text": "Renueva el idToken antes de que pase la hora y sustituye los dos tokens guardados. Ante un permission-denied, renueva una vez y reintenta la lectura una vez.", + "step4_link": "Renovar el idToken", + "how_code_title": "Las tres llamadas, según la documentación", + "how_code_note": "Los valores entre llaves proceden de la llamada anterior: apiKey y projectId de la configuración, uid e idToken del inicio de sesión.", + "int_title": "Integraciones documentadas en este repositorio", + "int_lead": "Cada guía de docs/clients cubre la autenticación, los datos que lee o escribe, la correspondencia de campos y el código. Los fragmentos siguientes están copiados de las guías.", + "ha_pill": "Esbozo de componente", + "ha_text": "Un componente personalizado de Home Assistant que expone un sensor por bobina, con el peso restante en gramos como estado. La guía también cubre los inventarios de los amigos, la paginación y el almacenamiento de tokens; el config flow y el sensor son esqueletos, y aún falta escribir un componente completamente funcional.", + "ha_sketch": "Esbozo de componente (sección 9)", + "esp_pill": "Sketch de Arduino", + "esp_text": "Para una báscula o un dispositivo NFC: busca una bobina por el UID RFID escaneado (uid y luego twin_uid), calcula el peso neto y escribe weight_available y last_update con una máscara de actualización. La guía termina con un sketch completo para ESP32 (WiFi, ArduinoJson).", + "py_pill": "Cliente completo", + "py_text": "Un cliente mínimo en Python basado en Pyrebase4: inicia sesión, lista tus bobinas y muestra cuántas bobinas tiene cada amigo. La guía contiene el script completo y un ejemplo de ejecución.", + "sm_pill": "Script completo", + "sm_text": "Un puente unidireccional de TigerTag a una instancia de Spoolman: las marcas se convierten en vendors, cada combinación de marca, material y color en un filament, y cada documento del inventario en un spool, creado o actualizado mediante la API REST de Spoolman. Se ejecuta desde cron, systemd o Docker.", + "scale_pill": "Contrato de dispositivo", + "scale_text": "El contrato que sigue una TigerScale o una báscula compatible para aparecer en línea en Tiger Studio Manager y en la aplicación móvil TigerTag: un documento de latido cada 30 segundos, una actualización de peso en cada lectura estable y las etiquetas gemelas escritas en un único lote atómico.", + "read_guide": "Leer la guía", + "example_folder": "Carpeta de ejemplo", + "int_note": "¿Desarrollas para otra plataforma? Una guía nueva va en docs/clients, y un ejemplo ejecutable opcional en examples/.", + "sec_title": "La cuenta del usuario, las reglas del usuario", + "sec_lead": "No hay ninguna clave compartida que proteger ni ningún token maestro que pueda filtrarse: un cliente puede hacer exactamente lo que puede hacer su usuario, y Firestore lo aplica en el servidor.", + "sec1_title": "Sin cuenta de servicio", + "sec1_text": "Cada cliente inicia sesión con las credenciales del propio usuario final. Nunca incluyas el SDK firebase-admin ni ninguna clave de administrador en un cliente de terceros.", + "sec2_title": "Las Security Rules deciden", + "sec2_text": "Las Firestore Security Rules comprueban cada solicitud frente a request.auth.uid: los propietarios leen y escriben sus propios documentos, los amigos pueden leer el inventario y las estanterías, y todo lo que no está permitido se deniega. La copia pública de las reglas está en rules/firestore.rules.", + "sec3_title": "Un vínculo por amistad", + "sec3_text": "Aceptar una solicitud de amistad escribe un documento en cada lado. Mientras exista users/{friend}/friends/{you}, puedes leer el inventario y las estanterías de ese amigo, en solo lectura; eliminarlo corta el acceso de inmediato.", + "sec4_title": "Sondeo respetuoso", + "sec4_text": "Sondea como máximo cada 5 minutos, nunca por debajo de 30 segundos, escribe solo cuando un valor cambie y pasa un límite en las lecturas de listas (200 bobinas, 50 estanterías). Firebase App Check no se aplica.", + "contract_title": "Un contrato público para integradores", + "contract_lead": "El repositorio se trata como un contrato público: los cambios que afectan a los clientes llegan a este repositorio en la misma versión, y el changelog registra cada uno como Added, Changed, Deprecated, Removed, Fixed o Security.", + "stable_title": "No cambiará sin previo aviso", + "stable1": "Las rutas de documentos como users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} y users/{uid}/friends/{friendUid}.", + "stable2": "El nombre y el significado de cada campo documentado: weight_available son gramos netos, last_update son milisegundos Unix, deleted: true indica un borrado lógico.", + "stable3": "El modelo de acceso de los amigos y los endpoints de Firebase Auth con los que inicia sesión un cliente.", + "stable4": "El apiKey y el projectId, salvo que se traslade el proyecto entero, lo que se anunciaría con al menos 6 meses de antelación.", + "may_title": "Puede cambiar: programa a la defensiva", + "may1": "Las tablas de referencia incorporan nuevas marcas y materiales; los identificadores numéricos solo se añaden y nunca se reutilizan.", + "may2": "Los endpoints HTTPS de conveniencia en cdn.tigertag.io pueden cambiar la forma de sus solicitudes y respuestas.", + "may3": "Las nuevas colecciones y los nuevos campos siempre se añaden sin romper nada: ignora los campos que no conozcas y seguirás siendo compatible.", + "contract_link": "Garantías de estabilidad en el README", + "docs_title": "Documentación de referencia", + "docs_lead": "Los documentos básicos sobre los que se construye toda integración, en el orden en que los necesita un cliente.", + "doc1": "El endpoint público de configuración de Firebase, para qué sirve cada campo y cuánto tiempo guardarlo en caché.", + "doc2": "Inicio de sesión con correo y contraseña, renovación del token y el patrón de reintento ante permission-denied.", + "doc3": "Cada colección y campo de Firestore: inventario, estanterías, amigos, perfiles, básculas.", + "doc4": "Cómo funciona el acceso de los amigos: códigos públicos, claves privadas, el vínculo bidireccional.", + "doc5": "Intervalos de sondeo, límites de consulta, supervisión y por qué App Check no se aplica.", + "doc6": "La copia pública de las Firestore Security Rules desplegadas.", + "eco_title": "Parte del ecosistema TigerTag", + "eco_lead": "Esta guía trata la cuenta en la nube. Los chips, las aplicaciones y los SDK tienen sus propios repositorios.", + "eco_proto_title": "El protocolo TigerTag", + "eco_proto_text": "El formato abierto de chip NFC para bobinas de filamento y botellas de resina, su especificación y la base de datos de referencia.", + "eco_studio_text": "La aplicación de escritorio (Electron) que gestiona el mismo inventario; la documentación se verifica con sus versiones.", + "eco_sdk_title": "SDK de TigerTag", + "eco_sdk_text": "Lee, verifica, crea y modifica chips TigerTag sin conexión, en JavaScript o Python, bajo Apache-2.0.", + "faq_title": "Preguntas", + "faq1_q": "¿Necesito una cuenta de servicio o una clave de administrador?", + "faq1_a": "No. Cada cliente inicia sesión con el correo y la contraseña TigerTag del propio usuario final, y las Firestore Security Rules deciden qué puede leer o escribir ese usuario. No existe ninguna clave de administrador compartida, y el SDK firebase-admin nunca debe usarse en un cliente de terceros.", + "faq2_q": "¿Por qué es público el apiKey de Firebase?", + "faq2_a": "El apiKey identifica el proyecto Firebase de TigerTag, no a quien llama. Sin un idToken válido, obtenido al iniciar sesión como un usuario real, no da acceso a nada; el control de acceso lo realizan la autenticación y las Security Rules.", + "faq3_q": "¿Puede Home Assistant o un ESP32 iniciar sesión con Google?", + "faq3_a": "No directamente: Google Sign-In necesita una redirección en el navegador. Un usuario que solo ha usado Google establece primero una contraseña en la cuenta, mediante la función de contraseña olvidada de la aplicación de escritorio o móvil, y después inicia sesión con correo y contraseña.", + "faq4_q": "¿Con qué frecuencia puede sondear mi integración?", + "faq4_a": "Como máximo cada 5 minutos para un sensor, cada 15 minutos para una vista general de estanterías, y nunca por debajo de 30 segundos. Una báscula solo escribe cuando cambia un peso. Las lecturas de listas deben pasar un límite: 200 para el inventario, 50 para las estanterías.", + "faq5_q": "¿Puede mi integración leer las bobinas de un amigo?", + "faq5_a": "Sí, en solo lectura. Cuando dos usuarios son amigos, cada uno tiene un documento del otro en users/{uid}/friends, y las reglas permiten a un amigo leer el inventario y las estanterías. Cualquiera de los dos puede eliminar la amistad en cualquier momento, lo que corta el acceso de inmediato.", + "faq6_q": "¿Y si la producción no coincide con esta documentación?", + "faq6_a": "Prevalece la documentación: una diferencia significa que se ha publicado una regresión. Abre una issue en el repositorio con la página, la ruta que leíste o escribiste y la respuesta, sin tokens ni claves privadas.", + "faq7_q": "¿Bajo qué licencia puedo reutilizar el código?", + "faq7_a": "Las Security Rules y todos los fragmentos de código de la documentación están bajo Apache-2.0, y no se exige atribución para los fragmentos que pegues en tu propio proyecto. La documentación en sí está bajo CC-BY-4.0. El nombre, el logotipo y el servicio TigerTag no están cubiertos por estas licencias.", + "join_title": "Empieza tu integración", + "join_text": "Elige la guía de tu plataforma en los enlaces rápidos del README y síguela desde el inicio de sesión hasta tu primera lectura.", + "join_btn": "Abrir los enlaces rápidos", + "foot_licences": "Documentación bajo CC-BY-4.0, código y fragmentos bajo Apache-2.0. El servicio TigerTag lo opera TigerTag Corp y no está cubierto por estas licencias.", + "foot_trademark": "TigerTag es una marca de TigerTag Corp. Política de marca", + "foot_source": "Código fuente de esta página", + "foot_issues": "Informar de un problema en la documentación", + "repo": "Repositorio", + "open": "Abrir" +} diff --git a/site/i18n/fr.json b/site/i18n/fr.json new file mode 100644 index 0000000..e8f4876 --- /dev/null +++ b/site/i18n/fr.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Français", + "skip": "Aller au contenu", + "theme_label": "Clair ou sombre", + "lang_label": "Choisir une langue", + "meta_title": "Intégration Firebase TigerTag : connectez votre client", + "meta_description": "Référence pour les clients Home Assistant, ESP32, Python et Spoolman qui lisent et mettent à jour les bobines TigerTag via Firebase Auth et Firestore.", + "og_title": "Guide d'intégration Firebase de TigerTag", + "og_description": "Connexion avec le compte TigerTag de l'utilisateur, lecture de ses bobines, mise à jour des poids : la référence des clients tiers, avec exemples.", + "og_alt": "Intégration Firebase TigerTag : la référence pour les clients Home Assistant, ESP32, Python et Spoolman", + "nav_how": "Fonctionnement", + "nav_integrations": "Intégrations", + "nav_security": "Sécurité", + "nav_contract": "Contrat", + "nav_docs": "Docs", + "nav_faq": "FAQ", + "hero_kicker": "Référence d'intégration tierce", + "hero_headline": "Connectez Home Assistant, une balance ESP32 ou un script Python à un compte TigerTag.", + "hero_lead": "Ce dépôt décrit comment n'importe quel client se connecte avec les identifiants TigerTag de l'utilisateur, lit ses bobines de filament et ses racks dans Firestore, et y réécrit le poids des bobines. Ce qui est documenté ici est ce qui est déployé, et chaque modification est consignée dans le changelog.", + "cta_start": "Démarrer une intégration", + "cta_examples": "Parcourir les exemples", + "cta_changelog": "Lire le changelog", + "flow_alt": "Comment un client se connecte : le client s'authentifie auprès de Firebase Authentication avec l'e-mail et le mot de passe de l'utilisateur, envoie l'idToken obtenu à Firestore, et les Firestore Security Rules décident de ce qu'il peut lire ou écrire dans users/{uid}/inventory.", + "flow_client": "Votre client", + "flow_auth": "Le compte de l'utilisateur, un idToken valable 1 heure", + "flow_rules": "Les propriétaires lisent et écrivent, les amis lisent", + "fact1_value": "0.1.3", + "fact1_label": "Dernière version publiée du contrat (CHANGELOG, 2 mai 2026)", + "fact2_value": "1 h", + "fact2_label": "Durée de vie d'un idToken : rafraîchissez-le avant son expiration", + "fact3_value": "5 min", + "fact3_label": "Intervalle d'interrogation minimal recommandé", + "fact4_value": "3 mois", + "fact4_label": "Préavis minimal de dépréciation avant qu'un champ soit renommé ou supprimé", + "how_title": "Comment un client se connecte à TigerTag", + "how_lead": "Tous les clients suivent les mêmes quatre étapes : récupérer la configuration Firebase publique, se connecter en tant qu'utilisateur, appeler l'API REST de Firestore avec le jeton de l'utilisateur, et garder ce jeton à jour.", + "step1_title": "Récupérer la configuration publique", + "step1_text": "Un seul GET renvoie la configuration du projet Firebase. L'apiKey est publique par conception : elle identifie le projet, pas l'appelant. Mettez-la en cache ; un rafraîchissement hebdomadaire suffit.", + "step2_title": "Se connecter en tant qu'utilisateur", + "step2_text": "L'e-mail et le mot de passe de l'utilisateur renvoient un UID, un idToken valable 1 heure et un refreshToken. Google Sign-In exige un navigateur : les clients sans interface utilisent donc un mot de passe.", + "step3_title": "Lire et écrire dans Firestore", + "step3_text": "Envoyez l'idToken comme jeton Bearer. Les bobines se trouvent dans users/{uid}/inventory, les racks dans users/{uid}/racks ; les poids sont en grammes nets, les dates en millisecondes Unix.", + "step4_title": "Garder le jeton à jour", + "step4_text": "Rafraîchissez l'idToken avant la fin de l'heure et remplacez les deux jetons stockés. En cas de permission-denied, rafraîchissez une fois et relancez la lecture une fois.", + "step4_link": "Rafraîchir l'idToken", + "how_code_title": "Les trois appels, tirés de la documentation", + "how_code_note": "Les valeurs entre accolades proviennent de l'appel précédent : apiKey et projectId de la configuration, uid et idToken de la connexion.", + "int_title": "Intégrations documentées dans ce dépôt", + "int_lead": "Chaque guide de docs/clients couvre l'authentification, les données lues ou écrites, la correspondance des champs et le code. Les extraits ci-dessous sont copiés des guides.", + "ha_pill": "Ébauche de composant", + "ha_text": "Un composant personnalisé Home Assistant qui expose un capteur par bobine, dont l'état est le poids restant en grammes. Le guide couvre aussi les inventaires des amis, la pagination et le stockage des jetons ; le config flow et le capteur sont des squelettes, et un composant entièrement fonctionnel reste à écrire.", + "ha_sketch": "Ébauche de composant (section 9)", + "esp_pill": "Sketch Arduino", + "esp_text": "Pour une balance ou un appareil NFC : retrouver une bobine à partir de l'UID RFID scanné (uid, puis twin_uid), calculer le poids net et écrire weight_available et last_update avec un masque de mise à jour. Le guide se termine par un sketch ESP32 complet (WiFi, ArduinoJson).", + "py_pill": "Client complet", + "py_text": "Un client Python minimal basé sur Pyrebase4 : il se connecte, liste vos bobines et affiche le nombre de bobines de chaque ami. Le guide contient le script entier et un exemple d'exécution.", + "sm_pill": "Script complet", + "sm_text": "Une passerelle à sens unique de TigerTag vers une instance Spoolman : les marques deviennent des fournisseurs, chaque combinaison marque, matériau et couleur devient un filament, et chaque document d'inventaire devient une bobine, créée ou mise à jour via l'API REST de Spoolman. À lancer depuis cron, systemd ou Docker.", + "scale_pill": "Contrat d'appareil", + "scale_text": "Le contrat que suit une TigerScale ou une balance compatible pour apparaître en ligne dans Tiger Studio Manager et l'application mobile TigerTag : un document de heartbeat toutes les 30 secondes, une mise à jour du poids à chaque mesure stable, et des tags jumeaux écrits en un seul lot atomique.", + "read_guide": "Lire le guide", + "example_folder": "Dossier d'exemple", + "int_note": "Vous développez pour une autre plateforme ? Un nouveau guide va dans docs/clients, un exemple exécutable facultatif dans examples/.", + "sec_title": "Le compte de l'utilisateur, les règles de l'utilisateur", + "sec_lead": "Aucune clé partagée à protéger, aucun jeton maître susceptible de fuir : un client peut faire exactement ce que son utilisateur peut faire, et Firestore l'impose côté serveur.", + "sec1_title": "Pas de compte de service", + "sec1_text": "Chaque client se connecte avec les identifiants de l'utilisateur final. N'embarquez jamais le SDK firebase-admin ni aucune clé d'administration dans un client tiers.", + "sec2_title": "Les Security Rules décident", + "sec2_text": "Les Firestore Security Rules vérifient chaque requête par rapport à request.auth.uid : les propriétaires lisent et écrivent leurs propres documents, les amis peuvent lire l'inventaire et les racks, et tout ce qui n'est pas autorisé est refusé. La copie publique des règles se trouve dans rules/firestore.rules.", + "sec3_title": "Un lien par amitié", + "sec3_text": "Accepter une demande d'ami écrit un document de chaque côté. Tant que users/{friend}/friends/{you} existe, vous pouvez lire l'inventaire et les racks de cet ami, en lecture seule ; le supprimer coupe l'accès immédiatement.", + "sec4_title": "Interrogation raisonnable", + "sec4_text": "Interrogez au plus toutes les 5 minutes, jamais en dessous de 30 secondes, n'écrivez que lorsqu'une valeur change, et passez une limite aux lectures de listes (200 bobines, 50 racks). Firebase App Check n'est pas imposé.", + "contract_title": "Un contrat public pour les intégrateurs", + "contract_lead": "Le dépôt est traité comme un contrat public : les changements qui touchent les clients arrivent dans ce dépôt dans la même version, et le changelog consigne chacun d'eux sous Added, Changed, Deprecated, Removed, Fixed ou Security.", + "stable_title": "Ne changera pas sans préavis", + "stable1": "Les chemins de documents tels que users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} et users/{uid}/friends/{friendUid}.", + "stable2": "Le nom et la signification de chaque champ documenté : weight_available est en grammes nets, last_update en millisecondes Unix, deleted: true signifie une suppression logique.", + "stable3": "Le modèle d'accès des amis, et les endpoints Firebase Auth avec lesquels un client se connecte.", + "stable4": "L'apiKey et le projectId, sauf si le projet entier déménage, ce qui serait annoncé au moins 6 mois à l'avance.", + "may_title": "Peut changer : soyez prudent", + "may1": "Les tables de référence s'enrichissent de nouvelles marques et de nouveaux matériaux ; les identifiants numériques ne font que s'ajouter et ne sont jamais réutilisés.", + "may2": "Les endpoints HTTPS de commodité sur cdn.tigertag.io peuvent faire évoluer la forme de leurs requêtes et réponses.", + "may3": "Les nouvelles collections et les nouveaux champs sont toujours additifs : ignorez les champs que vous ne connaissez pas et restez compatible.", + "contract_link": "Garanties de stabilité dans le README", + "docs_title": "Documentation de référence", + "docs_lead": "Les documents de base sur lesquels repose toute intégration, dans l'ordre où un client en a besoin.", + "doc1": "L'endpoint public de configuration Firebase, le rôle de chaque champ et la durée de mise en cache.", + "doc2": "Connexion par e-mail et mot de passe, rafraîchissement du jeton et schéma de nouvelle tentative sur permission-denied.", + "doc3": "Chaque collection et chaque champ Firestore : inventaire, racks, amis, profils, balances.", + "doc4": "Le fonctionnement de l'accès des amis : codes publics, clés privées, lien bidirectionnel.", + "doc5": "Intervalles d'interrogation, limites de requêtes, supervision, et pourquoi App Check n'est pas imposé.", + "doc6": "La copie publique des Firestore Security Rules déployées.", + "eco_title": "Partie de l'écosystème TigerTag", + "eco_lead": "Ce guide couvre le compte cloud. Les puces, les applications et les SDK ont leurs propres dépôts.", + "eco_proto_title": "Le protocole TigerTag", + "eco_proto_text": "Le format ouvert de puce NFC pour les bobines de filament et les flacons de résine, sa spécification et la base de données de référence.", + "eco_studio_text": "L'application de bureau (Electron) qui gère le même inventaire ; la documentation est vérifiée par rapport à ses versions.", + "eco_sdk_title": "SDK TigerTag", + "eco_sdk_text": "Lire, vérifier, créer et modifier des puces TigerTag hors ligne, en JavaScript ou en Python, sous Apache-2.0.", + "faq_title": "Questions", + "faq1_q": "Ai-je besoin d'un compte de service ou d'une clé d'administration ?", + "faq1_a": "Non. Chaque client se connecte avec l'e-mail et le mot de passe TigerTag de l'utilisateur final, et les Firestore Security Rules décident de ce que cet utilisateur peut lire ou écrire. Il n'existe aucune clé d'administration partagée, et le SDK firebase-admin ne doit jamais être utilisé dans un client tiers.", + "faq2_q": "Pourquoi l'apiKey Firebase est-elle publique ?", + "faq2_a": "L'apiKey identifie le projet Firebase de TigerTag, pas l'appelant. Sans idToken valide, obtenu en se connectant en tant qu'utilisateur réel, elle ne donne accès à rien ; le contrôle d'accès repose sur l'authentification et sur les Security Rules.", + "faq3_q": "Home Assistant ou un ESP32 peuvent-ils se connecter avec Google ?", + "faq3_a": "Pas directement : Google Sign-In nécessite une redirection dans un navigateur. Un utilisateur qui n'a jamais utilisé que Google définit d'abord un mot de passe sur son compte, via la procédure de mot de passe oublié de l'application de bureau ou mobile, puis se connecte avec son e-mail et son mot de passe.", + "faq4_q": "À quelle fréquence mon intégration peut-elle interroger ?", + "faq4_a": "Au plus toutes les 5 minutes pour un capteur, toutes les 15 minutes pour une vue d'ensemble des racks, et jamais en dessous de 30 secondes. Une balance n'écrit que lorsqu'un poids change. Les lectures de listes doivent passer une limite : 200 pour l'inventaire, 50 pour les racks.", + "faq5_q": "Mon intégration peut-elle lire les bobines d'un ami ?", + "faq5_a": "Oui, en lecture seule. Lorsque deux utilisateurs sont amis, chacun possède un document pour l'autre sous users/{uid}/friends, et les règles permettent à un ami de lire l'inventaire et les racks. Chacun des deux peut mettre fin à l'amitié à tout moment, ce qui coupe l'accès immédiatement.", + "faq6_q": "Et si la production ne correspond pas à cette documentation ?", + "faq6_a": "La documentation fait foi : un écart signifie qu'une régression a été livrée. Ouvrez une issue sur le dépôt avec la page, le chemin lu ou écrit et la réponse, en retirant les jetons et les clés privées.", + "faq7_q": "Sous quelle licence puis-je réutiliser le code ?", + "faq7_a": "Les Security Rules et tous les extraits de code de la documentation sont sous Apache-2.0, et l'attribution n'est pas exigée pour les extraits collés dans votre propre projet. La documentation elle-même est sous CC-BY-4.0. Le nom, le logo et le service TigerTag ne sont pas couverts par ces licences.", + "join_title": "Démarrez votre intégration", + "join_text": "Choisissez le guide de votre plateforme dans les liens rapides du README, et suivez-le de la connexion jusqu'à votre première lecture.", + "join_btn": "Ouvrir les liens rapides", + "foot_licences": "Documentation sous CC-BY-4.0, code et extraits sous Apache-2.0. Le service TigerTag est exploité par TigerTag Corp et n'est pas couvert par ces licences.", + "foot_trademark": "TigerTag est une marque de TigerTag Corp. Politique de marque", + "foot_source": "Source de cette page", + "foot_issues": "Signaler un problème de documentation", + "repo": "Dépôt", + "open": "Ouvrir" +} diff --git a/site/i18n/it.json b/site/i18n/it.json new file mode 100644 index 0000000..4584b50 --- /dev/null +++ b/site/i18n/it.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Italiano", + "skip": "Vai al contenuto", + "theme_label": "Chiaro o scuro", + "lang_label": "Scegli la lingua", + "meta_title": "Integrazione Firebase di TigerTag: collega il tuo client", + "meta_description": "Riferimento per client Home Assistant, ESP32, Python e Spoolman che leggono e aggiornano le bobine di filamento TigerTag tramite Firebase Auth e Firestore.", + "og_title": "Guida all'integrazione di TigerTag con Firebase", + "og_description": "Accedi con l'account TigerTag dell'utente stesso, leggi le sue bobine, aggiorna i pesi: il riferimento per i client di terze parti, con esempi.", + "og_alt": "Integrazione Firebase di TigerTag: il riferimento per i client Home Assistant, ESP32, Python e Spoolman", + "nav_how": "Come si collega", + "nav_integrations": "Integrazioni", + "nav_security": "Sicurezza", + "nav_contract": "Contratto", + "nav_docs": "Documentazione", + "nav_faq": "FAQ", + "hero_kicker": "Riferimento per integrazioni di terze parti", + "hero_headline": "Collega Home Assistant, una bilancia ESP32 o uno script Python a un account TigerTag.", + "hero_lead": "Questo repository documenta come qualsiasi client accede con le credenziali TigerTag dell'utente stesso, legge le sue bobine di filamento e i suoi scaffali in Firestore e riscrive il peso delle bobine. Ciò che è documentato qui è ciò che è in produzione, e ogni modifica è registrata nel changelog.", + "cta_start": "Inizia un'integrazione", + "cta_examples": "Sfoglia gli esempi", + "cta_changelog": "Leggi il changelog", + "flow_alt": "Come si collega un client: il client accede a Firebase Authentication con l'email e la password dell'utente, invia l'idToken restituito a Firestore, e le Firestore Security Rules decidono cosa può leggere o scrivere in users/{uid}/inventory.", + "flow_client": "Il tuo client", + "flow_auth": "L'account dell'utente stesso, un idToken da 1 ora", + "flow_rules": "I proprietari leggono e scrivono, gli amici leggono", + "fact1_value": "0.1.3", + "fact1_label": "Ultima versione rilasciata del contratto (CHANGELOG, 2 maggio 2026)", + "fact2_value": "1 h", + "fact2_label": "Durata di un idToken: rinnovalo prima che scada", + "fact3_value": "5 min", + "fact3_label": "Intervallo minimo di polling consigliato", + "fact4_value": "3 mesi", + "fact4_label": "Preavviso minimo di deprecazione prima che un campo venga rinominato o rimosso", + "how_title": "Come un client si collega a TigerTag", + "how_lead": "Ogni client segue gli stessi quattro passaggi: recuperare la configurazione pubblica di Firebase, accedere come l'utente, chiamare l'API REST di Firestore con il token dell'utente e mantenere valido quel token.", + "step1_title": "Recupera la configurazione pubblica", + "step1_text": "Una sola GET restituisce la configurazione del progetto Firebase. L'apiKey è pubblica per scelta progettuale: identifica il progetto, non chi effettua la chiamata. Mettila in cache; basta aggiornarla una volta alla settimana.", + "step2_title": "Accedi come l'utente", + "step2_text": "L'email e la password dell'utente stesso restituiscono un UID, un idToken da 1 ora e un refreshToken. Google Sign-In richiede un browser, quindi i client senza interfaccia usano una password.", + "step3_title": "Leggi e scrivi in Firestore", + "step3_text": "Invia l'idToken come token Bearer. Le bobine si trovano in users/{uid}/inventory, gli scaffali in users/{uid}/racks; i pesi sono grammi netti, le date millisecondi Unix.", + "step4_title": "Mantieni valido il token", + "step4_text": "Rinnova l'idToken prima che scada l'ora e sostituisci entrambi i token salvati. In caso di permission-denied, rinnova una volta e riprova la lettura una volta.", + "step4_link": "Rinnovare l'idToken", + "how_code_title": "Le tre chiamate, dalla documentazione", + "how_code_note": "I valori tra parentesi graffe provengono dalla chiamata precedente: apiKey e projectId dalla configurazione, uid e idToken dall'accesso.", + "int_title": "Integrazioni documentate in questo repository", + "int_lead": "Ogni guida in docs/clients copre l'autenticazione, i dati che legge o scrive, la mappatura dei campi e il codice. I frammenti qui sotto sono copiati dalle guide.", + "ha_pill": "Bozza di componente", + "ha_text": "Un componente personalizzato di Home Assistant che espone un sensore per bobina, con il peso residuo in grammi come stato. La guida copre anche gli inventari degli amici, la paginazione e la memorizzazione dei token; il config flow e il sensore sono scheletri, e un componente completamente funzionante è ancora da scrivere.", + "ha_sketch": "Bozza di componente (sezione 9)", + "esp_pill": "Sketch Arduino", + "esp_text": "Per una bilancia o un dispositivo NFC: cerca una bobina tramite l'UID RFID scansionato (uid, poi twin_uid), calcola il peso netto e scrivi weight_available e last_update con una maschera di aggiornamento. La guida si conclude con uno sketch completo per ESP32 (WiFi, ArduinoJson).", + "py_pill": "Client completo", + "py_text": "Un client Python minimale basato su Pyrebase4: accede, elenca le tue bobine e mostra quante bobine ha ciascun amico. La guida contiene lo script completo e un esempio di esecuzione.", + "sm_pill": "Script completo", + "sm_text": "Un ponte unidirezionale da TigerTag a un'istanza di Spoolman: i marchi diventano vendor, ogni combinazione di marchio, materiale e colore diventa un filament, e ogni documento dell'inventario diventa uno spool, creato o aggiornato tramite l'API REST di Spoolman. Si esegue da cron, systemd o Docker.", + "scale_pill": "Contratto del dispositivo", + "scale_text": "Il contratto che una TigerScale o una bilancia compatibile segue per apparire online in Tiger Studio Manager e nell'app mobile TigerTag: un documento di heartbeat ogni 30 secondi, un aggiornamento del peso a ogni lettura stabile e i tag gemelli scritti in un unico batch atomico.", + "read_guide": "Leggi la guida", + "example_folder": "Cartella di esempio", + "int_note": "Sviluppi per un'altra piattaforma? Una nuova guida va in docs/clients, un esempio eseguibile facoltativo in examples/.", + "sec_title": "L'account dell'utente, le regole dell'utente", + "sec_lead": "Non c'è nessuna chiave condivisa da proteggere né alcun token master che possa trapelare: un client può fare esattamente ciò che può fare il suo utente, e Firestore lo impone sul server.", + "sec1_title": "Nessun account di servizio", + "sec1_text": "Ogni client accede con le credenziali dell'utente finale stesso. Non includere mai l'SDK firebase-admin né alcuna chiave di amministrazione in un client di terze parti.", + "sec2_title": "Decidono le Security Rules", + "sec2_text": "Le Firestore Security Rules verificano ogni richiesta rispetto a request.auth.uid: i proprietari leggono e scrivono i propri documenti, gli amici possono leggere l'inventario e gli scaffali, e tutto ciò che non è consentito viene negato. La copia pubblica delle regole si trova in rules/firestore.rules.", + "sec3_title": "Un legame per ogni amicizia", + "sec3_text": "Accettare una richiesta di amicizia scrive un documento su ciascun lato. Finché esiste users/{friend}/friends/{you} puoi leggere l'inventario e gli scaffali di quell'amico, in sola lettura; eliminarlo interrompe subito l'accesso.", + "sec4_title": "Polling rispettoso", + "sec4_text": "Interroga al massimo ogni 5 minuti, mai sotto i 30 secondi, scrivi solo quando un valore cambia e passa un limite nelle letture di elenchi (200 bobine, 50 scaffali). Firebase App Check non è imposto.", + "contract_title": "Un contratto pubblico per gli integratori", + "contract_lead": "Il repository è trattato come un contratto pubblico: le modifiche che riguardano i client arrivano in questo repository nella stessa release, e il changelog registra ciascuna come Added, Changed, Deprecated, Removed, Fixed o Security.", + "stable_title": "Non cambierà senza preavviso", + "stable1": "I percorsi dei documenti come users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} e users/{uid}/friends/{friendUid}.", + "stable2": "Il nome e il significato di ogni campo documentato: weight_available è in grammi netti, last_update in millisecondi Unix, deleted: true indica un'eliminazione logica.", + "stable3": "Il modello di accesso degli amici e gli endpoint di Firebase Auth con cui un client accede.", + "stable4": "L'apiKey e il projectId, a meno che l'intero progetto non venga trasferito, cosa annunciata con almeno 6 mesi di anticipo.", + "may_title": "Può cambiare: programma in modo difensivo", + "may1": "Le tabelle di riferimento si arricchiscono di nuovi marchi e materiali; gli id numerici vengono solo aggiunti e mai riutilizzati.", + "may2": "Gli endpoint HTTPS di comodo su cdn.tigertag.io possono evolvere la forma delle richieste e delle risposte.", + "may3": "Le nuove collezioni e i nuovi campi sono sempre aggiuntivi: ignora i campi che non conosci e rimarrai compatibile.", + "contract_link": "Garanzie di stabilità nel README", + "docs_title": "Documentazione di riferimento", + "docs_lead": "I documenti di base su cui si fonda ogni integrazione, nell'ordine in cui servono a un client.", + "doc1": "L'endpoint pubblico della configurazione Firebase, a cosa serve ciascun campo e per quanto tempo tenerlo in cache.", + "doc2": "Accesso con email e password, rinnovo del token e lo schema di nuovo tentativo in caso di permission-denied.", + "doc3": "Ogni collezione e campo di Firestore: inventario, scaffali, amici, profili, bilance.", + "doc4": "Come funziona l'accesso degli amici: codici pubblici, chiavi private, il legame bidirezionale.", + "doc5": "Intervalli di polling, limiti delle query, monitoraggio e perché App Check non è imposto.", + "doc6": "La copia pubblica delle Firestore Security Rules in produzione.", + "eco_title": "Parte dell'ecosistema TigerTag", + "eco_lead": "Questa guida riguarda l'account cloud. I chip, le app e gli SDK hanno i propri repository.", + "eco_proto_title": "Il protocollo TigerTag", + "eco_proto_text": "Il formato aperto di chip NFC per bobine di filamento e flaconi di resina, la sua specifica e il database di riferimento.", + "eco_studio_text": "L'app desktop (Electron) che gestisce lo stesso inventario; la documentazione è verificata rispetto alle sue release.", + "eco_sdk_title": "SDK TigerTag", + "eco_sdk_text": "Leggi, verifica, crea e modifica chip TigerTag offline, in JavaScript o Python, sotto Apache-2.0.", + "faq_title": "Domande", + "faq1_q": "Mi serve un account di servizio o una chiave di amministrazione?", + "faq1_a": "No. Ogni client accede con l'email e la password TigerTag dell'utente finale stesso, e le Firestore Security Rules decidono cosa quell'utente può leggere o scrivere. Non esiste alcuna chiave di amministrazione condivisa, e l'SDK firebase-admin non deve mai essere usato in un client di terze parti.", + "faq2_q": "Perché l'apiKey di Firebase è pubblica?", + "faq2_a": "L'apiKey identifica il progetto Firebase di TigerTag, non chi effettua la chiamata. Senza un idToken valido, ottenuto accedendo come un utente reale, non dà accesso a nulla; il controllo degli accessi è affidato all'autenticazione e alle Security Rules.", + "faq3_q": "Home Assistant o un ESP32 possono accedere con Google?", + "faq3_a": "Non direttamente: Google Sign-In richiede un reindirizzamento nel browser. Un utente che ha sempre usato solo Google imposta prima una password sull'account, tramite la procedura di password dimenticata dell'app desktop o mobile, e poi accede con email e password.", + "faq4_q": "Con quale frequenza può interrogare la mia integrazione?", + "faq4_a": "Al massimo ogni 5 minuti per un sensore, ogni 15 minuti per una panoramica degli scaffali, e mai sotto i 30 secondi. Una bilancia scrive solo quando un peso cambia. Le letture di elenchi devono passare un limite: 200 per l'inventario, 50 per gli scaffali.", + "faq5_q": "La mia integrazione può leggere le bobine di un amico?", + "faq5_a": "Sì, in sola lettura. Quando due utenti sono amici, ciascuno ha un documento per l'altro in users/{uid}/friends, e le regole consentono a un amico di leggere l'inventario e gli scaffali. Ciascuno dei due può rimuovere l'amicizia in qualsiasi momento, interrompendo subito l'accesso.", + "faq6_q": "E se la produzione non corrisponde a questa documentazione?", + "faq6_a": "Vale la documentazione: una differenza significa che è stata rilasciata una regressione. Apri una issue sul repository con la pagina, il percorso che hai letto o scritto e la risposta, rimuovendo token e chiavi private.", + "faq7_q": "Con quale licenza posso riutilizzare il codice?", + "faq7_a": "Le Security Rules e ogni frammento di codice della documentazione sono sotto Apache-2.0, e l'attribuzione non è richiesta per i frammenti incollati nel tuo progetto. La documentazione stessa è sotto CC-BY-4.0. Il nome, il logo e il servizio TigerTag non sono coperti da queste licenze.", + "join_title": "Inizia la tua integrazione", + "join_text": "Scegli la guida per la tua piattaforma nei link rapidi del README e seguila dall'accesso alla tua prima lettura.", + "join_btn": "Apri i link rapidi", + "foot_licences": "Documentazione sotto CC-BY-4.0, codice e frammenti sotto Apache-2.0. Il servizio TigerTag è gestito da TigerTag Corp e non è coperto da queste licenze.", + "foot_trademark": "TigerTag è un marchio di TigerTag Corp. Politica sul marchio", + "foot_source": "Sorgente di questa pagina", + "foot_issues": "Segnala un problema nella documentazione", + "repo": "Repository", + "open": "Apri" +} diff --git a/site/i18n/nl.json b/site/i18n/nl.json new file mode 100644 index 0000000..ebd26c7 --- /dev/null +++ b/site/i18n/nl.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Nederlands", + "skip": "Naar de inhoud", + "theme_label": "Licht of donker", + "lang_label": "Kies een taal", + "meta_title": "TigerTag Firebase-integratie: koppel je eigen client", + "meta_description": "Referentie voor Home Assistant-, ESP32-, Python- en Spoolman-clients die TigerTag-filamentspoelen lezen en bijwerken via Firebase Auth en Firestore.", + "og_title": "Handleiding voor de TigerTag Firebase-integratie", + "og_description": "Inloggen met het eigen TigerTag-account van de gebruiker, spoelen lezen, gewichten bijwerken: de referentie voor clients van derden, met voorbeelden.", + "og_alt": "TigerTag Firebase-integratie: de referentie voor Home Assistant-, ESP32-, Python- en Spoolman-clients", + "nav_how": "Hoe het verbindt", + "nav_integrations": "Integraties", + "nav_security": "Beveiliging", + "nav_contract": "Contract", + "nav_docs": "Documentatie", + "nav_faq": "FAQ", + "hero_kicker": "Referentie voor integraties van derden", + "hero_headline": "Verbind Home Assistant, een ESP32-weegschaal of een Python-script met een TigerTag-account.", + "hero_lead": "Deze repository beschrijft hoe elke client inlogt met de eigen TigerTag-inloggegevens van de gebruiker, diens filamentspoelen en rekken in Firestore leest en spoelgewichten terugschrijft. Wat hier gedocumenteerd is, is wat er draait, en elke wijziging daarin wordt vastgelegd in de changelog.", + "cta_start": "Begin een integratie", + "cta_examples": "Bekijk de voorbeelden", + "cta_changelog": "Lees de changelog", + "flow_alt": "Hoe een client verbinding maakt: de client logt in bij Firebase Authentication met het e-mailadres en wachtwoord van de gebruiker, stuurt het ontvangen idToken naar Firestore, en Firestore Security Rules bepalen wat hij mag lezen of schrijven in users/{uid}/inventory.", + "flow_client": "Jouw client", + "flow_auth": "Het eigen account van de gebruiker, een idToken van 1 uur", + "flow_rules": "Eigenaars lezen en schrijven, vrienden lezen", + "fact1_value": "0.1.3", + "fact1_label": "Nieuwste uitgebrachte versie van het contract (CHANGELOG, 2 mei 2026)", + "fact2_value": "1 h", + "fact2_label": "Geldigheidsduur van een idToken: vernieuw het voordat het verloopt", + "fact3_value": "5 min", + "fact3_label": "Aanbevolen minimale polling-interval", + "fact4_value": "3 maanden", + "fact4_label": "Minimale aankondigingstermijn voordat een veld wordt hernoemd of verwijderd", + "how_title": "Hoe een client verbinding maakt met TigerTag", + "how_lead": "Elke client volgt dezelfde vier stappen: de openbare Firebase-configuratie ophalen, inloggen als de gebruiker, de Firestore REST API aanroepen met het token van de gebruiker, en dat token geldig houden.", + "step1_title": "Haal de openbare configuratie op", + "step1_text": "Eén GET geeft de configuratie van het Firebase-project terug. De apiKey is bewust openbaar: hij identificeert het project, niet de aanroeper. Sla hem in de cache op; wekelijks vernieuwen is genoeg.", + "step2_title": "Log in als de gebruiker", + "step2_text": "Het eigen e-mailadres en wachtwoord van de gebruiker leveren een UID, een idToken van 1 uur en een refreshToken op. Google Sign-In heeft een browser nodig, dus headless clients gebruiken een wachtwoord.", + "step3_title": "Lees en schrijf in Firestore", + "step3_text": "Stuur het idToken mee als Bearer-token. Spoelen staan in users/{uid}/inventory, rekken in users/{uid}/racks; gewichten zijn netto grammen, datums Unix-milliseconden.", + "step4_title": "Houd het token geldig", + "step4_text": "Vernieuw het idToken voordat het uur om is en vervang beide opgeslagen tokens. Bij een permission-denied vernieuw je één keer en probeer je de leesactie één keer opnieuw.", + "step4_link": "Het idToken vernieuwen", + "how_code_title": "De drie aanroepen, uit de documentatie", + "how_code_note": "De waarden tussen accolades komen uit de vorige aanroep: apiKey en projectId uit de configuratie, uid en idToken uit het inloggen.", + "int_title": "Integraties die in deze repository zijn gedocumenteerd", + "int_lead": "Elke handleiding in docs/clients behandelt de authenticatie, de gegevens die worden gelezen of geschreven, de veldtoewijzing en de code. De fragmenten hieronder komen uit de handleidingen.", + "ha_pill": "Schets van een component", + "ha_text": "Een custom component voor Home Assistant die per spoel één sensor aanbiedt, met het resterende gewicht in grammen als status. De handleiding behandelt ook de inventarissen van vrienden, paginering en de opslag van tokens; de config flow en de sensor zijn skeletten, en een volledig werkend component moet nog worden geschreven.", + "ha_sketch": "Schets van het component (sectie 9)", + "esp_pill": "Arduino-sketch", + "esp_text": "Voor een weegschaal of NFC-apparaat: zoek een spoel op aan de hand van de gescande RFID-UID (uid, daarna twin_uid), bereken het nettogewicht en schrijf weight_available en last_update met een update mask. De handleiding eindigt met een volledige ESP32-sketch (WiFi, ArduinoJson).", + "py_pill": "Volledige client", + "py_text": "Een minimale Python-client op basis van Pyrebase4: hij logt in, toont je spoelen en geeft weer hoeveel spoelen elke vriend heeft. De handleiding bevat het volledige script en een voorbeelduitvoer.", + "sm_pill": "Volledig script", + "sm_text": "Een eenrichtingsbrug van TigerTag naar een Spoolman-instantie: merken worden leveranciers, elke combinatie van merk, materiaal en kleur wordt een filament, en elk inventarisdocument wordt een spoel, aangemaakt of bijgewerkt via de REST API van Spoolman. Draai het vanuit cron, systemd of Docker.", + "scale_pill": "Apparaatcontract", + "scale_text": "Het contract dat een TigerScale of compatibele weegschaal volgt om online te verschijnen in Tiger Studio Manager en de mobiele TigerTag-app: elke 30 seconden een heartbeat-document, een gewichtsupdate bij elke stabiele meting, en twin-tags die in één atomaire batch worden geschreven.", + "read_guide": "Lees de handleiding", + "example_folder": "Voorbeeldmap", + "int_note": "Bouw je voor een ander platform? Een nieuwe handleiding hoort in docs/clients, een optioneel werkend voorbeeld in examples/.", + "sec_title": "Het account van de gebruiker, de regels van de gebruiker", + "sec_lead": "Er is geen gedeelde sleutel om te beschermen en geen mastertoken dat kan uitlekken: een client kan precies wat zijn gebruiker kan, en Firestore dwingt dat af op de server.", + "sec1_title": "Geen serviceaccount", + "sec1_text": "Elke client logt in met de eigen inloggegevens van de eindgebruiker. Lever de firebase-admin SDK of een beheerderssleutel nooit mee in een client van derden.", + "sec2_title": "Security Rules beslissen", + "sec2_text": "Firestore Security Rules controleren elk verzoek aan de hand van request.auth.uid: eigenaars lezen en schrijven hun eigen documenten, vrienden mogen de inventaris en rekken lezen, en alles wat niet is toegestaan wordt geweigerd. De openbare kopie van de regels staat in rules/firestore.rules.", + "sec3_title": "Eén band per vriendschap", + "sec3_text": "Het accepteren van een vriendschapsverzoek schrijft aan beide kanten een document. Zolang users/{friend}/friends/{you} bestaat, mag je de inventaris en rekken van die vriend lezen, alleen-lezen; het verwijderen ervan beëindigt de toegang onmiddellijk.", + "sec4_title": "Beleefd pollen", + "sec4_text": "Poll hooguit elke 5 minuten, nooit vaker dan elke 30 seconden, schrijf alleen als een waarde verandert, en geef een limiet mee bij het lezen van lijsten (200 spoelen, 50 rekken). Firebase App Check wordt niet afgedwongen.", + "contract_title": "Een openbaar contract voor integrators", + "contract_lead": "De repository wordt behandeld als een openbaar contract: wijzigingen die clients raken komen in dezelfde release in deze repository terecht, en de changelog legt elke wijziging vast als Added, Changed, Deprecated, Removed, Fixed of Security.", + "stable_title": "Verandert niet zonder aankondiging", + "stable1": "Documentpaden zoals users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} en users/{uid}/friends/{friendUid}.", + "stable2": "De naam en betekenis van elk gedocumenteerd veld: weight_available is netto grammen, last_update is Unix-milliseconden, deleted: true betekent soft-deleted.", + "stable3": "Het toegangsmodel voor vrienden, en de Firebase Auth-endpoints waarmee een client inlogt.", + "stable4": "De apiKey en projectId, tenzij het hele project verhuist, wat minstens 6 maanden van tevoren wordt aangekondigd.", + "may_title": "Kan veranderen: programmeer defensief", + "may1": "Referentietabellen krijgen nieuwe merken en materialen; numerieke id's worden alleen toegevoegd en nooit hergebruikt.", + "may2": "Handige HTTPS-endpoints op cdn.tigertag.io kunnen de vorm van hun verzoeken en antwoorden aanpassen.", + "may3": "Nieuwe collecties en nieuwe velden zijn altijd aanvullend: negeer de velden die je niet kent en blijf compatibel.", + "contract_link": "Stabiliteitsgaranties in de README", + "docs_title": "Referentiedocumentatie", + "docs_lead": "De kerndocumenten waarop elke integratie voortbouwt, in de volgorde waarin een client ze nodig heeft.", + "doc1": "Het openbare endpoint voor de Firebase-configuratie, waarvoor elk veld dient en hoe lang je het in de cache bewaart.", + "doc2": "Inloggen met e-mail en wachtwoord, tokens vernieuwen, en het patroon voor opnieuw proberen na permission-denied.", + "doc3": "Elke Firestore-collectie en elk veld: inventaris, rekken, vrienden, profielen, weegschalen.", + "doc4": "Hoe toegang voor vrienden werkt: openbare codes, privésleutels, de wederzijdse band.", + "doc5": "Polling-intervallen, querylimieten, monitoring, en waarom App Check niet wordt afgedwongen.", + "doc6": "De openbare kopie van de uitgerolde Firestore Security Rules.", + "eco_title": "Onderdeel van het TigerTag-ecosysteem", + "eco_lead": "Deze handleiding gaat over het cloudaccount. De chips, de apps en de SDK's hebben hun eigen repository's.", + "eco_proto_title": "Het TigerTag-protocol", + "eco_proto_text": "Het open NFC-chipformaat voor filamentspoelen en harsflessen, de specificatie ervan en de referentiedatabase.", + "eco_studio_text": "De desktopapp (Electron) die dezelfde inventaris beheert; de documentatie wordt getoetst aan de releases ervan.", + "eco_sdk_title": "TigerTag-SDK's", + "eco_sdk_text": "Lees, verifieer, maak en wijzig TigerTag-chips offline, in JavaScript of Python, onder Apache-2.0.", + "faq_title": "Vragen", + "faq1_q": "Heb ik een serviceaccount of een beheerderssleutel nodig?", + "faq1_a": "Nee. Elke client logt in met het eigen TigerTag-e-mailadres en -wachtwoord van de eindgebruiker, en Firestore Security Rules bepalen wat die gebruiker mag lezen of schrijven. Er is geen gedeelde beheerderssleutel, en de firebase-admin SDK mag nooit in een client van derden worden gebruikt.", + "faq2_q": "Waarom is de Firebase-apiKey openbaar?", + "faq2_a": "De apiKey identificeert het Firebase-project van TigerTag, niet de aanroeper. Zonder geldig idToken, verkregen door in te loggen als een echte gebruiker, geeft hij nergens toegang toe; de toegangscontrole gebeurt via authenticatie en de Security Rules.", + "faq3_q": "Kan Home Assistant of een ESP32 inloggen met Google?", + "faq3_a": "Niet rechtstreeks: Google Sign-In vereist een doorverwijzing in de browser. Een gebruiker die altijd alleen Google heeft gebruikt, stelt eerst een wachtwoord in voor het account via de wachtwoord-vergeten-functie van de desktop- of mobiele app, en logt daarna in met e-mail en wachtwoord.", + "faq4_q": "Hoe vaak mag mijn integratie pollen?", + "faq4_a": "Hooguit elke 5 minuten voor een sensor, elke 15 minuten voor een overzicht van rekken, en nooit vaker dan elke 30 seconden. Een weegschaal schrijft alleen als een gewicht verandert. Bij het lezen van lijsten hoort een limiet: 200 voor de inventaris, 50 voor rekken.", + "faq5_q": "Kan mijn integratie de spoelen van een vriend lezen?", + "faq5_a": "Ja, alleen-lezen. Als twee gebruikers vrienden zijn, heeft elk van hen een document voor de ander onder users/{uid}/friends, en de regels laten een vriend de inventaris en rekken lezen. Elk van beide gebruikers kan de vriendschap op elk moment beëindigen, wat de toegang onmiddellijk afsluit.", + "faq6_q": "Wat als de productie afwijkt van deze documentatie?", + "faq6_a": "De documentatie heeft voorrang: een verschil betekent dat er een regressie is uitgerold. Open een issue in de repository met de pagina, het pad dat je las of schreef en het antwoord, met tokens en privésleutels verwijderd.", + "faq7_q": "Onder welke licentie mag ik de code hergebruiken?", + "faq7_a": "De Security Rules en elk codefragment in de documentatie vallen onder Apache-2.0, en naamsvermelding is niet vereist voor fragmenten die je in je eigen project plakt. De documentatie zelf valt onder CC-BY-4.0. De naam, het logo en de dienst TigerTag vallen niet onder deze licenties.", + "join_title": "Begin je integratie", + "join_text": "Kies de handleiding voor jouw platform in de snelkoppelingen van de README, en volg die van het inloggen tot je eerste leesactie.", + "join_btn": "Open de snelkoppelingen", + "foot_licences": "Documentatie onder CC-BY-4.0, code en fragmenten onder Apache-2.0. De dienst TigerTag wordt beheerd door TigerTag Corp en valt niet onder deze licenties.", + "foot_trademark": "TigerTag is een handelsmerk van TigerTag Corp. Merkenbeleid", + "foot_source": "Bron van deze pagina", + "foot_issues": "Meld een probleem met de documentatie", + "repo": "Repository", + "open": "Openen" +} diff --git a/site/i18n/pl.json b/site/i18n/pl.json new file mode 100644 index 0000000..5aa3fa1 --- /dev/null +++ b/site/i18n/pl.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Polski", + "skip": "Przejdź do treści", + "theme_label": "Jasny lub ciemny", + "lang_label": "Wybierz język", + "meta_title": "TigerTag Firebase Integration: połącz własnego klienta", + "meta_description": "Dokumentacja klientów Home Assistant, ESP32, Python i Spoolman, które odczytują i aktualizują szpule filamentu TigerTag przez Firebase Auth i Firestore.", + "og_title": "Przewodnik po integracji TigerTag z Firebase", + "og_description": "Logowanie własnym kontem TigerTag użytkownika, odczyt jego szpul, aktualizacja wagi: dokumentacja dla klientów zewnętrznych, z przykładami.", + "og_alt": "TigerTag Firebase Integration: dokumentacja dla klientów Home Assistant, ESP32, Python i Spoolman", + "nav_how": "Jak to się łączy", + "nav_integrations": "Integracje", + "nav_security": "Bezpieczeństwo", + "nav_contract": "Kontrakt", + "nav_docs": "Dokumentacja", + "nav_faq": "FAQ", + "hero_kicker": "Dokumentacja integracji zewnętrznych", + "hero_headline": "Połącz Home Assistant, wagę na ESP32 lub skrypt w Pythonie z kontem TigerTag.", + "hero_lead": "To repozytorium opisuje, jak dowolny klient loguje się własnymi danymi logowania użytkownika do TigerTag, odczytuje jego szpule filamentu i regały w Firestore oraz zapisuje z powrotem wagę szpul. To, co jest tu udokumentowane, jest tym, co działa na produkcji, a każda zmiana jest odnotowywana w dzienniku zmian.", + "cta_start": "Rozpocznij integrację", + "cta_examples": "Przeglądaj przykłady", + "cta_changelog": "Przeczytaj dziennik zmian", + "flow_alt": "Jak łączy się klient: klient loguje się do Firebase Authentication adresem e-mail i hasłem użytkownika, wysyła otrzymany idToken do Firestore, a reguły Firestore Security Rules decydują, co może odczytać lub zapisać w users/{uid}/inventory.", + "flow_client": "Twój klient", + "flow_auth": "Własne konto użytkownika, idToken ważny 1 godzinę", + "flow_rules": "Właściciele odczytują i zapisują, znajomi odczytują", + "fact1_value": "0.1.3", + "fact1_label": "Najnowsza wydana wersja kontraktu (CHANGELOG, 2 maja 2026)", + "fact2_value": "1 h", + "fact2_label": "Czas ważności idToken: odśwież go, zanim wygaśnie", + "fact3_value": "5 min", + "fact3_label": "Zalecany minimalny odstęp między odpytywaniami", + "fact4_value": "3 miesiące", + "fact4_label": "Minimalny okres wycofywania przed zmianą nazwy lub usunięciem pola", + "how_title": "Jak klient łączy się z TigerTag", + "how_lead": "Każdy klient wykonuje te same cztery kroki: pobiera publiczną konfigurację Firebase, loguje się jako użytkownik, wywołuje Firestore REST API z tokenem użytkownika i dba o to, by ten token był aktualny.", + "step1_title": "Pobierz publiczną konfigurację", + "step1_text": "Jedno żądanie GET zwraca konfigurację projektu Firebase. apiKey jest z założenia publiczny: identyfikuje projekt, a nie wywołującego. Zapisz go w pamięci podręcznej; wystarczy odświeżanie raz w tygodniu.", + "step2_title": "Zaloguj się jako użytkownik", + "step2_text": "Własny e-mail i hasło użytkownika zwracają UID, idToken ważny 1 godzinę i refreshToken. Google Sign-In wymaga przeglądarki, dlatego klienci bez interfejsu używają hasła.", + "step3_title": "Odczytuj i zapisuj w Firestore", + "step3_text": "Wysyłaj idToken jako token Bearer. Szpule znajdują się w users/{uid}/inventory, regały w users/{uid}/racks; wagi to gramy netto, daty to milisekundy Unix.", + "step4_title": "Dbaj o aktualność tokena", + "step4_text": "Odśwież idToken przed upływem godziny i zastąp oba zapisane tokeny. Przy błędzie permission-denied odśwież raz i ponów odczyt raz.", + "step4_link": "Odświeżanie idToken", + "how_code_title": "Trzy wywołania z dokumentacji", + "how_code_note": "Wartości w nawiasach klamrowych pochodzą z poprzedniego wywołania: apiKey i projectId z konfiguracji, uid i idToken z logowania.", + "int_title": "Integracje udokumentowane w tym repozytorium", + "int_lead": "Każdy przewodnik w docs/clients opisuje uwierzytelnianie, dane, które odczytuje lub zapisuje, mapowanie pól i kod. Poniższe fragmenty pochodzą z przewodników.", + "ha_pill": "Szkic komponentu", + "ha_text": "Niestandardowy komponent Home Assistant, który udostępnia jeden czujnik na szpulę, a jego stanem jest pozostała waga w gramach. Przewodnik opisuje też inwentarze znajomych, paginację i przechowywanie tokenów; config flow i czujnik są szkieletami, a w pełni działający komponent nie został jeszcze napisany.", + "ha_sketch": "Szkic komponentu (sekcja 9)", + "esp_pill": "Szkic Arduino", + "esp_text": "Dla wagi lub urządzenia NFC: wyszukaj szpulę po zeskanowanym UID RFID (uid, a następnie twin_uid), oblicz wagę netto i zapisz weight_available oraz last_update z maską aktualizacji. Przewodnik kończy się kompletnym szkicem dla ESP32 (WiFi, ArduinoJson).", + "py_pill": "Kompletny klient", + "py_text": "Minimalny klient w Pythonie oparty na Pyrebase4: loguje się, wyświetla listę Twoich szpul i pokazuje, ile szpul ma każdy znajomy. Przewodnik zawiera cały skrypt i przykładowe uruchomienie.", + "sm_pill": "Kompletny skrypt", + "sm_text": "Jednokierunkowy most z TigerTag do instancji Spoolman: marki stają się dostawcami, każde połączenie marki, materiału i koloru staje się filamentem, a każdy dokument inwentarza staje się szpulą, tworzoną lub aktualizowaną przez REST API Spoolmana. Uruchamiaj go z cron, systemd lub Dockera.", + "scale_pill": "Kontrakt urządzenia", + "scale_text": "Kontrakt, którego przestrzega TigerScale lub kompatybilna waga, aby była widoczna online w Tiger Studio Manager i aplikacji mobilnej TigerTag: dokument heartbeat co 30 sekund, aktualizacja wagi przy każdym stabilnym odczycie i bliźniacze tagi zapisywane w jednej atomowej partii.", + "read_guide": "Przeczytaj przewodnik", + "example_folder": "Folder z przykładem", + "int_note": "Tworzysz dla innej platformy? Nowy przewodnik trafia do docs/clients, opcjonalny działający przykład do examples/.", + "sec_title": "Konto użytkownika, reguły użytkownika", + "sec_lead": "Nie ma wspólnego klucza do ochrony ani tokena głównego, który mógłby wyciec: klient może dokładnie to, co jego użytkownik, a Firestore egzekwuje to po stronie serwera.", + "sec1_title": "Bez konta usługi", + "sec1_text": "Każdy klient loguje się własnymi danymi logowania użytkownika końcowego. Nigdy nie dołączaj SDK firebase-admin ani żadnego klucza administratora do klienta zewnętrznego.", + "sec2_title": "Decydują Security Rules", + "sec2_text": "Firestore Security Rules sprawdzają każde żądanie względem request.auth.uid: właściciele odczytują i zapisują własne dokumenty, znajomi mogą odczytywać inwentarz i regały, a wszystko, co nie jest dozwolone, jest odrzucane. Publiczna kopia reguł znajduje się w rules/firestore.rules.", + "sec3_title": "Jedna więź na znajomość", + "sec3_text": "Przyjęcie zaproszenia do znajomych zapisuje dokument po każdej stronie. Dopóki istnieje users/{friend}/friends/{you}, możesz odczytywać inwentarz i regały tego znajomego, tylko do odczytu; usunięcie dokumentu natychmiast odcina dostęp.", + "sec4_title": "Kulturalne odpytywanie", + "sec4_text": "Odpytuj najczęściej co 5 minut, nigdy częściej niż co 30 sekund, zapisuj tylko wtedy, gdy wartość się zmienia, i podawaj limit przy odczytach list (200 szpul, 50 regałów). Firebase App Check nie jest wymuszany.", + "contract_title": "Publiczny kontrakt dla integratorów", + "contract_lead": "Repozytorium jest traktowane jako publiczny kontrakt: zmiany wpływające na klientów trafiają do tego repozytorium w tym samym wydaniu, a dziennik zmian zapisuje każdą z nich jako Added, Changed, Deprecated, Removed, Fixed lub Security.", + "stable_title": "Nie zmieni się bez uprzedzenia", + "stable1": "Ścieżki dokumentów, takie jak users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} i users/{uid}/friends/{friendUid}.", + "stable2": "Nazwa i znaczenie każdego udokumentowanego pola: weight_available to gramy netto, last_update to milisekundy Unix, deleted: true oznacza miękkie usunięcie.", + "stable3": "Model dostępu znajomych oraz punkty końcowe Firebase Auth, przez które loguje się klient.", + "stable4": "apiKey i projectId, chyba że cały projekt zostanie przeniesiony, co zostanie ogłoszone co najmniej 6 miesięcy wcześniej.", + "may_title": "Może się zmienić: programuj defensywnie", + "may1": "Tabele referencyjne zyskują nowe marki i materiały; identyfikatory liczbowe są tylko dopisywane i nigdy nie są używane ponownie.", + "may2": "Pomocnicze punkty końcowe HTTPS w cdn.tigertag.io mogą zmieniać kształt żądań i odpowiedzi.", + "may3": "Nowe kolekcje i nowe pola są zawsze dodawane: ignoruj pola, których nie znasz, i zachowaj zgodność.", + "contract_link": "Gwarancje stabilności w README", + "docs_title": "Dokumentacja referencyjna", + "docs_lead": "Podstawowe dokumenty, na których opiera się każda integracja, w kolejności, w jakiej potrzebuje ich klient.", + "doc1": "Publiczny punkt końcowy konfiguracji Firebase, przeznaczenie każdego pola i jak długo go przechowywać w pamięci podręcznej.", + "doc2": "Logowanie e-mailem i hasłem, odświeżanie tokena i wzorzec ponawiania po permission-denied.", + "doc3": "Każda kolekcja i każde pole Firestore: inwentarz, regały, znajomi, profile, wagi.", + "doc4": "Jak działa dostęp znajomych: kody publiczne, klucze prywatne, dwukierunkowa więź.", + "doc5": "Odstępy odpytywania, limity zapytań, monitorowanie i dlaczego App Check nie jest wymuszany.", + "doc6": "Publiczna kopia wdrożonych Firestore Security Rules.", + "eco_title": "Część ekosystemu TigerTag", + "eco_lead": "Ten przewodnik dotyczy konta w chmurze. Chipy, aplikacje i SDK mają własne repozytoria.", + "eco_proto_title": "Protokół TigerTag", + "eco_proto_text": "Otwarty format chipów NFC dla szpul filamentu i butelek żywicy, jego specyfikacja i referencyjna baza danych.", + "eco_studio_text": "Aplikacja na komputer (Electron), która zarządza tym samym inwentarzem; dokumentacja jest weryfikowana względem jej wydań.", + "eco_sdk_title": "SDK TigerTag", + "eco_sdk_text": "Odczytuj, weryfikuj, twórz i modyfikuj chipy TigerTag offline, w JavaScript lub Pythonie, na licencji Apache-2.0.", + "faq_title": "Pytania", + "faq1_q": "Czy potrzebuję konta usługi lub klucza administratora?", + "faq1_a": "Nie. Każdy klient loguje się własnym adresem e-mail i hasłem TigerTag użytkownika końcowego, a Firestore Security Rules decydują, co ten użytkownik może odczytać lub zapisać. Nie ma wspólnego klucza administratora, a SDK firebase-admin nigdy nie może być używany w kliencie zewnętrznym.", + "faq2_q": "Dlaczego apiKey Firebase jest publiczny?", + "faq2_a": "apiKey identyfikuje projekt Firebase TigerTag, a nie wywołującego. Bez ważnego idToken, uzyskanego przez zalogowanie się jako prawdziwy użytkownik, nie daje dostępu do niczego; kontrolę dostępu zapewniają uwierzytelnianie i Security Rules.", + "faq3_q": "Czy Home Assistant lub ESP32 mogą logować się przez Google?", + "faq3_a": "Nie bezpośrednio: Google Sign-In wymaga przekierowania w przeglądarce. Użytkownik, który zawsze korzystał tylko z Google, najpierw ustawia hasło do konta przez funkcję przypomnienia hasła w aplikacji na komputer lub w aplikacji mobilnej, a następnie loguje się e-mailem i hasłem.", + "faq4_q": "Jak często moja integracja może odpytywać?", + "faq4_a": "Najczęściej co 5 minut dla czujnika, co 15 minut dla przeglądu regałów i nigdy częściej niż co 30 sekund. Waga zapisuje dane tylko wtedy, gdy zmienia się ciężar. Odczyty list powinny podawać limit: 200 dla inwentarza, 50 dla regałów.", + "faq5_q": "Czy moja integracja może odczytać szpule znajomego?", + "faq5_a": "Tak, tylko do odczytu. Gdy dwoje użytkowników jest znajomymi, każdy z nich ma dokument dotyczący drugiego w users/{uid}/friends, a reguły pozwalają znajomemu odczytywać inwentarz i regały. Każdy z użytkowników może w dowolnej chwili usunąć znajomość, co natychmiast odcina dostęp.", + "faq6_q": "Co, jeśli produkcja nie zgadza się z tą dokumentacją?", + "faq6_a": "Rozstrzyga dokumentacja: różnica oznacza, że wdrożono regresję. Otwórz zgłoszenie w repozytorium, podając stronę, ścieżkę, którą odczytywano lub zapisywano, oraz odpowiedź, po usunięciu tokenów i kluczy prywatnych.", + "faq7_q": "Na jakiej licencji mogę ponownie wykorzystać kod?", + "faq7_a": "Security Rules i każdy fragment kodu w dokumentacji są na licencji Apache-2.0, a wymóg podania autorstwa nie dotyczy fragmentów wklejonych do własnego projektu. Sama dokumentacja jest na licencji CC-BY-4.0. Nazwa, logo i usługa TigerTag nie są objęte tymi licencjami.", + "join_title": "Rozpocznij swoją integrację", + "join_text": "Wybierz przewodnik dla swojej platformy w szybkich linkach README i przejdź przez niego od logowania do pierwszego odczytu.", + "join_btn": "Otwórz szybkie linki", + "foot_licences": "Dokumentacja na licencji CC-BY-4.0, kod i fragmenty kodu na licencji Apache-2.0. Usługę TigerTag obsługuje TigerTag Corp i nie jest ona objęta tymi licencjami.", + "foot_trademark": "TigerTag jest znakiem towarowym TigerTag Corp. Polityka znaków towarowych", + "foot_source": "Źródło tej strony", + "foot_issues": "Zgłoś problem z dokumentacją", + "repo": "Repozytorium", + "open": "Otwórz" +} diff --git a/site/i18n/pt-pt.json b/site/i18n/pt-pt.json new file mode 100644 index 0000000..dab6463 --- /dev/null +++ b/site/i18n/pt-pt.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Português (PT)", + "skip": "Saltar para o conteúdo", + "theme_label": "Claro ou escuro", + "lang_label": "Escolher um idioma", + "meta_title": "Integração Firebase TigerTag: ligue o seu próprio cliente", + "meta_description": "Referência para clientes Home Assistant, ESP32, Python e Spoolman que leem e atualizam as bobinas de filamento TigerTag através do Firebase Auth e Firestore.", + "og_title": "Guia de integração Firebase do TigerTag", + "og_description": "Inicie sessão com a conta TigerTag do próprio utilizador, leia as suas bobinas e atualize os pesos: a referência para clientes de terceiros, com exemplos.", + "og_alt": "Integração Firebase TigerTag: a referência para clientes Home Assistant, ESP32, Python e Spoolman", + "nav_how": "Como se liga", + "nav_integrations": "Integrações", + "nav_security": "Segurança", + "nav_contract": "Contrato", + "nav_docs": "Documentação", + "nav_faq": "Perguntas frequentes", + "hero_kicker": "Referência de integração para terceiros", + "hero_headline": "Ligar o Home Assistant, uma balança ESP32 ou um script Python a uma conta TigerTag.", + "hero_lead": "Este repositório documenta a forma como qualquer cliente inicia sessão com as credenciais TigerTag do próprio utilizador, lê as respetivas bobinas de filamento e prateleiras no Firestore e volta a escrever o peso das bobinas. O que aqui está documentado é o que está implementado, e cada alteração fica registada no changelog.", + "cta_start": "Iniciar uma integração", + "cta_examples": "Consultar os exemplos", + "cta_changelog": "Ler o changelog", + "flow_alt": "Como um cliente se liga: o cliente inicia sessão no Firebase Authentication com o e-mail e a palavra-passe do utilizador, envia o idToken devolvido ao Firestore, e as Firestore Security Rules determinam o que pode ler ou escrever em users/{uid}/inventory.", + "flow_client": "O seu cliente", + "flow_auth": "A conta do próprio utilizador, um idToken de 1 hora", + "flow_rules": "Os proprietários leem e escrevem, os amigos leem", + "fact1_value": "0.1.3", + "fact1_label": "Versão mais recente publicada do contrato (CHANGELOG, 2 de maio de 2026)", + "fact2_value": "1 h", + "fact2_label": "Duração de um idToken: renove-o antes de expirar", + "fact3_value": "5 min", + "fact3_label": "Intervalo mínimo de consulta recomendado", + "fact4_value": "3 meses", + "fact4_label": "Pré-aviso mínimo de descontinuação antes de um campo ser renomeado ou removido", + "how_title": "Como um cliente se liga ao TigerTag", + "how_lead": "Todos os clientes seguem os mesmos quatro passos: obter a configuração pública do Firebase, iniciar sessão como o utilizador, chamar a API REST do Firestore com o token do utilizador e manter esse token atualizado.", + "step1_title": "Obter a configuração pública", + "step1_text": "Um único GET devolve a configuração do projeto Firebase. A apiKey é pública por conceção: identifica o projeto, não quem faz o pedido. Guarde-a em cache; basta atualizá-la semanalmente.", + "step2_title": "Iniciar sessão como o utilizador", + "step2_text": "O e-mail e a palavra-passe do próprio utilizador devolvem um UID, um idToken de 1 hora e um refreshToken. O Google Sign-In requer um navegador, pelo que os clientes sem interface gráfica usam uma palavra-passe.", + "step3_title": "Ler e escrever no Firestore", + "step3_text": "Envie o idToken como token Bearer. As bobinas encontram-se em users/{uid}/inventory e as prateleiras em users/{uid}/racks; os pesos são gramas líquidas e as datas milissegundos Unix.", + "step4_title": "Manter o token atualizado", + "step4_text": "Renove o idToken antes de passar a hora e substitua os dois tokens guardados. Perante um permission-denied, renove uma vez e repita a leitura uma vez.", + "step4_link": "Renovar o idToken", + "how_code_title": "As três chamadas, retiradas da documentação", + "how_code_note": "Os valores entre chavetas provêm da chamada anterior: apiKey e projectId da configuração, uid e idToken do início de sessão.", + "int_title": "Integrações documentadas neste repositório", + "int_lead": "Cada guia em docs/clients trata da autenticação, dos dados que lê ou escreve, do mapeamento de campos e do código. Os excertos abaixo foram copiados dos guias.", + "ha_pill": "Esboço de componente", + "ha_text": "Um componente personalizado do Home Assistant que disponibiliza um sensor por bobina, tendo como estado o peso restante em gramas. O guia aborda também os inventários dos amigos, a paginação e o armazenamento dos tokens; o config flow e o sensor são esqueletos, e falta ainda escrever um componente totalmente funcional.", + "ha_sketch": "Esboço de componente (secção 9)", + "esp_pill": "Sketch Arduino", + "esp_text": "Para uma balança ou um dispositivo NFC: procure uma bobina pelo UID RFID lido (uid e, a seguir, twin_uid), calcule o peso líquido e escreva weight_available e last_update com uma update mask. O guia termina com um sketch ESP32 completo (WiFi, ArduinoJson).", + "py_pill": "Cliente completo", + "py_text": "Um cliente Python mínimo assente no Pyrebase4: inicia sessão, lista as suas bobinas e indica quantas bobinas tem cada amigo. O guia contém o script completo e um exemplo de execução.", + "sm_pill": "Script completo", + "sm_text": "Uma ponte num só sentido do TigerTag para uma instância do Spoolman: as marcas passam a fornecedores, cada combinação de marca, material e cor passa a filamento, e cada documento de inventário passa a bobina, criado ou atualizado através da API REST do Spoolman. Pode ser executado a partir de cron, systemd ou Docker.", + "scale_pill": "Contrato de dispositivo", + "scale_text": "O contrato que uma TigerScale ou balança compatível cumpre para surgir online no Tiger Studio Manager e na aplicação móvel TigerTag: um documento de heartbeat a cada 30 segundos, uma atualização de peso em cada leitura estável e etiquetas gémeas escritas num único lote atómico.", + "read_guide": "Ler o guia", + "example_folder": "Pasta de exemplo", + "int_note": "A desenvolver para outra plataforma? Um novo guia vai para docs/clients e um exemplo executável opcional para examples/.", + "sec_title": "A conta do utilizador, as regras do utilizador", + "sec_lead": "Não há nenhuma chave partilhada a proteger nem nenhum token mestre que possa ser divulgado: um cliente pode fazer exatamente o que o seu utilizador pode fazer, e o Firestore impõe-no no servidor.", + "sec1_title": "Sem conta de serviço", + "sec1_text": "Todos os clientes iniciam sessão com as credenciais do próprio utilizador final. Nunca inclua o SDK firebase-admin nem qualquer chave de administrador num cliente de terceiros.", + "sec2_title": "As Security Rules decidem", + "sec2_text": "As Firestore Security Rules verificam cada pedido face a request.auth.uid: os proprietários leem e escrevem os seus próprios documentos, os amigos podem ler o inventário e as prateleiras, e tudo o que não for permitido é recusado. A cópia pública das regras encontra-se em rules/firestore.rules.", + "sec3_title": "Um vínculo por amizade", + "sec3_text": "Aceitar um pedido de amizade escreve um documento de cada lado. Enquanto users/{friend}/friends/{you} existir, pode ler o inventário e as prateleiras desse amigo, apenas em leitura; eliminá-lo corta o acesso de imediato.", + "sec4_title": "Consultas comedidas", + "sec4_text": "Consulte no máximo a cada 5 minutos, nunca abaixo de 30 segundos, escreva apenas quando um valor mudar e indique um limite nas leituras de listas (200 bobinas, 50 prateleiras). O Firebase App Check não é imposto.", + "contract_title": "Um contrato público para integradores", + "contract_lead": "O repositório é tratado como um contrato público: as alterações que afetam os clientes chegam a este repositório na mesma versão, e o changelog regista cada uma como Added, Changed, Deprecated, Removed, Fixed ou Security.", + "stable_title": "Não muda sem pré-aviso", + "stable1": "Caminhos de documentos como users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} e users/{uid}/friends/{friendUid}.", + "stable2": "O nome e o significado de cada campo documentado: weight_available está em gramas líquidas, last_update em milissegundos Unix, deleted: true significa eliminação lógica.", + "stable3": "O modelo de acesso entre amigos e os endpoints do Firebase Auth com que um cliente inicia sessão.", + "stable4": "A apiKey e o projectId, salvo se todo o projeto for migrado, o que será anunciado com pelo menos 6 meses de antecedência.", + "may_title": "Pode mudar: programe de forma defensiva", + "may1": "As tabelas de referência recebem novas marcas e materiais; os ids numéricos apenas são acrescentados e nunca reutilizados.", + "may2": "Os endpoints HTTPS de conveniência em cdn.tigertag.io podem alterar o formato dos pedidos e das respostas.", + "may3": "As novas coleções e os novos campos são sempre aditivos: ignore os campos que não conhece e mantenha-se compatível.", + "contract_link": "Garantias de estabilidade no README", + "docs_title": "Documentação de referência", + "docs_lead": "Os documentos essenciais em que assenta qualquer integração, pela ordem em que um cliente precisa deles.", + "doc1": "O endpoint público de configuração do Firebase, a função de cada campo e durante quanto tempo o guardar em cache.", + "doc2": "Início de sessão com e-mail e palavra-passe, renovação do token e o padrão de nova tentativa após permission-denied.", + "doc3": "Todas as coleções e campos do Firestore: inventory, racks, friends, perfis, balanças.", + "doc4": "Como funciona o acesso entre amigos: códigos públicos, chaves privadas, o vínculo bidirecional.", + "doc5": "Intervalos de consulta, limites das queries, monitorização e o motivo pelo qual o App Check não é imposto.", + "doc6": "A cópia pública das Firestore Security Rules implementadas.", + "eco_title": "Parte do ecossistema TigerTag", + "eco_lead": "Este guia trata da conta na nuvem. Os chips, as aplicações e os SDK têm os seus próprios repositórios.", + "eco_proto_title": "O protocolo TigerTag", + "eco_proto_text": "O formato aberto de chip NFC para bobinas de filamento e frascos de resina, a respetiva especificação e a base de dados de referência.", + "eco_studio_text": "A aplicação de computador (Electron) que gere o mesmo inventário; a documentação é verificada face às suas versões.", + "eco_sdk_title": "SDK TigerTag", + "eco_sdk_text": "Ler, verificar, criar e modificar chips TigerTag sem ligação, em JavaScript ou Python, sob Apache-2.0.", + "faq_title": "Perguntas", + "faq1_q": "Preciso de uma conta de serviço ou de uma chave de administrador?", + "faq1_a": "Não. Todos os clientes iniciam sessão com o e-mail e a palavra-passe TigerTag do próprio utilizador final, e as Firestore Security Rules determinam o que esse utilizador pode ler ou escrever. Não existe nenhuma chave de administrador partilhada, e o SDK firebase-admin nunca deve ser utilizado num cliente de terceiros.", + "faq2_q": "Porque é que a apiKey do Firebase é pública?", + "faq2_a": "A apiKey identifica o projeto Firebase do TigerTag, não quem faz o pedido. Sem um idToken válido, obtido ao iniciar sessão como um utilizador real, não dá acesso a nada; o controlo de acesso é assegurado pela autenticação e pelas Security Rules.", + "faq3_q": "O Home Assistant ou um ESP32 podem iniciar sessão com o Google?", + "faq3_a": "Não diretamente: o Google Sign-In requer um redirecionamento no navegador. Um utilizador que sempre usou apenas o Google define primeiro uma palavra-passe na conta, através da opção de palavra-passe esquecida da aplicação de computador ou móvel, e depois inicia sessão com e-mail e palavra-passe.", + "faq4_q": "Com que frequência pode a minha integração fazer consultas?", + "faq4_a": "No máximo a cada 5 minutos para um sensor, a cada 15 minutos para uma vista geral das prateleiras, e nunca abaixo de 30 segundos. Uma balança só escreve quando um peso muda. As leituras de listas devem indicar um limite: 200 para o inventário, 50 para as prateleiras.", + "faq5_q": "A minha integração pode ler as bobinas de um amigo?", + "faq5_a": "Sim, apenas em leitura. Quando dois utilizadores são amigos, cada um tem um documento relativo ao outro em users/{uid}/friends, e as regras permitem que um amigo leia o inventário e as prateleiras. Qualquer um dos dois pode pôr fim à amizade a qualquer momento, o que corta o acesso de imediato.", + "faq6_q": "E se a produção não coincidir com esta documentação?", + "faq6_a": "Prevalece a documentação: uma diferença significa que foi publicada uma regressão. Abra uma issue no repositório com a página, o caminho que leu ou escreveu e a resposta, depois de retirar os tokens e as chaves privadas.", + "faq7_q": "Ao abrigo de que licença posso reutilizar o código?", + "faq7_a": "As Security Rules e todos os excertos de código da documentação estão sob Apache-2.0, sendo dispensada a atribuição para excertos colados no seu próprio projeto. A documentação propriamente dita está sob CC-BY-4.0. O nome, o logótipo e o serviço TigerTag não estão abrangidos por estas licenças.", + "join_title": "Comece a sua integração", + "join_text": "Escolha o guia para a sua plataforma nas ligações rápidas do README e siga-o desde o início de sessão até à primeira leitura.", + "join_btn": "Abrir as ligações rápidas", + "foot_licences": "Documentação sob CC-BY-4.0, código e excertos sob Apache-2.0. O serviço TigerTag é operado pela TigerTag Corp e não está abrangido por estas licenças.", + "foot_trademark": "TigerTag é uma marca da TigerTag Corp. Política de marca", + "foot_source": "Código-fonte desta página", + "foot_issues": "Comunicar um problema na documentação", + "repo": "Repositório", + "open": "Abrir" +} diff --git a/site/i18n/pt.json b/site/i18n/pt.json new file mode 100644 index 0000000..ac2fa81 --- /dev/null +++ b/site/i18n/pt.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Português (BR)", + "skip": "Pular para o conteúdo", + "theme_label": "Claro ou escuro", + "lang_label": "Escolha um idioma", + "meta_title": "Integração TigerTag Firebase: conecte seu próprio cliente", + "meta_description": "Referência para clientes Home Assistant, ESP32, Python e Spoolman que leem e atualizam carretéis de filamento TigerTag via Firebase Auth e Firestore.", + "og_title": "Guia de integração TigerTag com Firebase", + "og_description": "Faça login com a conta TigerTag do próprio usuário, leia os carretéis dele e atualize os pesos: a referência para clientes de terceiros, com exemplos.", + "og_alt": "Integração TigerTag Firebase: a referência para clientes Home Assistant, ESP32, Python e Spoolman", + "nav_how": "Como funciona a conexão", + "nav_integrations": "Integrações", + "nav_security": "Segurança", + "nav_contract": "Contrato", + "nav_docs": "Documentação", + "nav_faq": "Perguntas frequentes", + "hero_kicker": "Referência de integração para terceiros", + "hero_headline": "Conecte o Home Assistant, uma balança ESP32 ou um script Python a uma conta TigerTag.", + "hero_lead": "Este repositório documenta como qualquer cliente faz login com as credenciais TigerTag do próprio usuário, lê os carretéis de filamento e os racks dele no Firestore e grava de volta o peso dos carretéis. O que está documentado aqui é o que está em produção, e cada alteração é registrada no changelog.", + "cta_start": "Começar uma integração", + "cta_examples": "Ver os exemplos", + "cta_changelog": "Ler o changelog", + "flow_alt": "Como um cliente se conecta: o cliente faz login no Firebase Authentication com o e-mail e a senha do usuário, envia o idToken retornado ao Firestore, e as Firestore Security Rules decidem o que ele pode ler ou gravar em users/{uid}/inventory.", + "flow_client": "Seu cliente", + "flow_auth": "A conta do próprio usuário, um idToken de 1 hora", + "flow_rules": "Donos leem e gravam, amigos leem", + "fact1_value": "0.1.3", + "fact1_label": "Última versão publicada do contrato (CHANGELOG, 2 de maio de 2026)", + "fact2_value": "1 h", + "fact2_label": "Validade de um idToken: renove-o antes que expire", + "fact3_value": "5 min", + "fact3_label": "Intervalo mínimo de consulta recomendado", + "fact4_value": "3 meses", + "fact4_label": "Aviso mínimo de descontinuação antes de um campo ser renomeado ou removido", + "how_title": "Como um cliente se conecta ao TigerTag", + "how_lead": "Todo cliente segue os mesmos quatro passos: buscar a configuração pública do Firebase, fazer login como o usuário, chamar a API REST do Firestore com o token do usuário e manter esse token válido.", + "step1_title": "Buscar a configuração pública", + "step1_text": "Um único GET retorna a configuração do projeto Firebase. O apiKey é público por design: ele identifica o projeto, não quem faz a chamada. Guarde-o em cache; uma atualização semanal é suficiente.", + "step2_title": "Fazer login como o usuário", + "step2_text": "O e-mail e a senha do próprio usuário retornam um UID, um idToken de 1 hora e um refreshToken. O Google Sign-In exige um navegador, então clientes sem interface usam uma senha.", + "step3_title": "Ler e gravar no Firestore", + "step3_text": "Envie o idToken como token Bearer. Os carretéis ficam em users/{uid}/inventory e os racks em users/{uid}/racks; os pesos são gramas líquidos, e as datas, milissegundos Unix.", + "step4_title": "Manter o token válido", + "step4_text": "Renove o idToken antes de completar uma hora e substitua os dois tokens armazenados. Em caso de permission-denied, renove uma vez e repita a leitura uma vez.", + "step4_link": "Renovando o idToken", + "how_code_title": "As três chamadas, tiradas da documentação", + "how_code_note": "Os valores entre chaves vêm da chamada anterior: apiKey e projectId da configuração, uid e idToken do login.", + "int_title": "Integrações documentadas neste repositório", + "int_lead": "Cada guia em docs/clients aborda a autenticação, os dados que lê ou grava, o mapeamento de campos e o código. Os trechos abaixo foram copiados dos guias.", + "ha_pill": "Esboço de componente", + "ha_text": "Um componente personalizado do Home Assistant que expõe um sensor por carretel, com o peso restante em gramas como estado. O guia também aborda os inventários de amigos, a paginação e o armazenamento de tokens; o config flow e o sensor são esqueletos, e um componente totalmente executável ainda precisa ser escrito.", + "ha_sketch": "Esboço de componente (seção 9)", + "esp_pill": "Sketch Arduino", + "esp_text": "Para uma balança ou um dispositivo NFC: localize um carretel pelo UID RFID lido (uid, depois twin_uid), calcule o peso líquido e grave weight_available e last_update com uma update mask. O guia termina com um sketch ESP32 completo (WiFi, ArduinoJson).", + "py_pill": "Cliente completo", + "py_text": "Um cliente Python mínimo baseado no Pyrebase4: ele faz login, lista seus carretéis e mostra quantos carretéis cada amigo tem. O guia traz o script inteiro e um exemplo de execução.", + "sm_pill": "Script completo", + "sm_text": "Uma ponte unidirecional do TigerTag para uma instância do Spoolman: as marcas viram fornecedores, cada combinação de marca, material e cor vira um filamento, e cada documento de inventário vira um carretel, criado ou atualizado pela API REST do Spoolman. Execute-o via cron, systemd ou Docker.", + "scale_pill": "Contrato de dispositivo", + "scale_text": "O contrato que uma TigerScale ou balança compatível segue para aparecer online no Tiger Studio Manager e no app móvel TigerTag: um documento de heartbeat a cada 30 segundos, uma atualização de peso a cada leitura estável e tags gêmeas gravadas em um único lote atômico.", + "read_guide": "Ler o guia", + "example_folder": "Pasta de exemplo", + "int_note": "Desenvolvendo para outra plataforma? Um novo guia vai em docs/clients, e um exemplo executável opcional em examples/.", + "sec_title": "A conta do usuário, as regras do usuário", + "sec_lead": "Não existe chave compartilhada para proteger nem token mestre que possa vazar: um cliente pode fazer exatamente o que seu usuário pode fazer, e o Firestore garante isso no servidor.", + "sec1_title": "Nenhuma conta de serviço", + "sec1_text": "Todo cliente faz login com as credenciais do próprio usuário final. Nunca distribua o SDK firebase-admin nem qualquer chave de administrador em um cliente de terceiros.", + "sec2_title": "As Security Rules decidem", + "sec2_text": "As Firestore Security Rules verificam cada requisição em relação a request.auth.uid: donos leem e gravam os próprios documentos, amigos podem ler o inventário e os racks, e tudo o que não é permitido é negado. A cópia pública das regras está em rules/firestore.rules.", + "sec3_title": "Um vínculo por amizade", + "sec3_text": "Aceitar um pedido de amizade grava um documento de cada lado. Enquanto users/{friend}/friends/{you} existir, você pode ler o inventário e os racks desse amigo, somente leitura; excluí-lo corta o acesso imediatamente.", + "sec4_title": "Consultas moderadas", + "sec4_text": "Consulte no máximo a cada 5 minutos, nunca em menos de 30 segundos, grave apenas quando um valor mudar e passe um limite nas leituras de listas (200 carretéis, 50 racks). O Firebase App Check não é exigido.", + "contract_title": "Um contrato público para integradores", + "contract_lead": "O repositório é tratado como um contrato público: as mudanças que afetam os clientes chegam a este repositório na mesma versão, e o changelog registra cada uma como Added, Changed, Deprecated, Removed, Fixed ou Security.", + "stable_title": "Não muda sem aviso prévio", + "stable1": "Caminhos de documentos como users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} e users/{uid}/friends/{friendUid}.", + "stable2": "O nome e o significado de cada campo documentado: weight_available é em gramas líquidos, last_update em milissegundos Unix, deleted: true significa exclusão lógica.", + "stable3": "O modelo de acesso entre amigos e os endpoints do Firebase Auth usados pelo cliente para fazer login.", + "stable4": "O apiKey e o projectId, a menos que o projeto inteiro mude, o que seria anunciado com pelo menos 6 meses de antecedência.", + "may_title": "Pode mudar: programe de forma defensiva", + "may1": "As tabelas de referência ganham novas marcas e materiais; os ids numéricos só são acrescentados e nunca reutilizados.", + "may2": "Os endpoints HTTPS de conveniência em cdn.tigertag.io podem evoluir o formato das requisições e respostas.", + "may3": "Novas coleções e novos campos são sempre aditivos: ignore os campos que você não conhece e continue compatível.", + "contract_link": "Garantias de estabilidade no README", + "docs_title": "Documentação de referência", + "docs_lead": "Os documentos centrais em que toda integração se baseia, na ordem em que um cliente precisa deles.", + "doc1": "O endpoint público de configuração do Firebase, para que serve cada campo e por quanto tempo mantê-lo em cache.", + "doc2": "Login com e-mail e senha, renovação de token e o padrão de nova tentativa após permission-denied.", + "doc3": "Todas as coleções e campos do Firestore: inventory, racks, friends, perfis, balanças.", + "doc4": "Como funciona o acesso entre amigos: códigos públicos, chaves privadas, o vínculo bidirecional.", + "doc5": "Intervalos de consulta, limites de query, monitoramento e por que o App Check não é exigido.", + "doc6": "A cópia pública das Firestore Security Rules em produção.", + "eco_title": "Parte do ecossistema TigerTag", + "eco_lead": "Este guia trata da conta na nuvem. Os chips, os apps e os SDKs têm seus próprios repositórios.", + "eco_proto_title": "O protocolo TigerTag", + "eco_proto_text": "O formato aberto de chip NFC para carretéis de filamento e frascos de resina, sua especificação e o banco de dados de referência.", + "eco_studio_text": "O app de desktop (Electron) que gerencia o mesmo inventário; a documentação é verificada a cada versão dele.", + "eco_sdk_title": "SDKs TigerTag", + "eco_sdk_text": "Leia, verifique, crie e modifique chips TigerTag offline, em JavaScript ou Python, sob Apache-2.0.", + "faq_title": "Perguntas", + "faq1_q": "Preciso de uma conta de serviço ou de uma chave de administrador?", + "faq1_a": "Não. Todo cliente faz login com o e-mail e a senha TigerTag do próprio usuário final, e as Firestore Security Rules decidem o que esse usuário pode ler ou gravar. Não existe chave de administrador compartilhada, e o SDK firebase-admin nunca deve ser usado em um cliente de terceiros.", + "faq2_q": "Por que o apiKey do Firebase é público?", + "faq2_a": "O apiKey identifica o projeto Firebase do TigerTag, não quem faz a chamada. Sem um idToken válido, obtido ao fazer login como um usuário real, ele não dá acesso a nada; o controle de acesso é feito pela autenticação e pelas Security Rules.", + "faq3_q": "O Home Assistant ou um ESP32 podem fazer login com o Google?", + "faq3_a": "Não diretamente: o Google Sign-In exige um redirecionamento no navegador. Um usuário que sempre usou apenas o Google primeiro define uma senha na conta, pelo fluxo de senha esquecida do app de desktop ou do app móvel, e depois faz login com e-mail e senha.", + "faq4_q": "Com que frequência minha integração pode fazer consultas?", + "faq4_a": "No máximo a cada 5 minutos para um sensor, a cada 15 minutos para uma visão geral dos racks, e nunca em menos de 30 segundos. Uma balança grava apenas quando um peso muda. As leituras de listas devem passar um limite: 200 para o inventário, 50 para os racks.", + "faq5_q": "Minha integração pode ler os carretéis de um amigo?", + "faq5_a": "Sim, somente leitura. Quando dois usuários são amigos, cada um tem um documento referente ao outro em users/{uid}/friends, e as regras permitem que um amigo leia o inventário e os racks. Qualquer um dos dois pode desfazer a amizade a qualquer momento, o que corta o acesso imediatamente.", + "faq6_q": "E se a produção divergir desta documentação?", + "faq6_a": "A documentação prevalece: uma diferença significa que uma regressão foi publicada. Abra uma issue no repositório com a página, o caminho que você leu ou gravou e a resposta, removendo tokens e chaves privadas.", + "faq7_q": "Sob qual licença posso reutilizar o código?", + "faq7_a": "As Security Rules e todos os trechos de código da documentação estão sob Apache-2.0, e a atribuição é dispensada para trechos colados no seu próprio projeto. A documentação em si está sob CC-BY-4.0. O nome, o logotipo e o serviço TigerTag não são cobertos por essas licenças.", + "join_title": "Comece sua integração", + "join_text": "Escolha o guia da sua plataforma nos links rápidos do README e siga-o do login até a sua primeira leitura.", + "join_btn": "Abrir os links rápidos", + "foot_licences": "Documentação sob CC-BY-4.0, código e trechos sob Apache-2.0. O serviço TigerTag é operado pela TigerTag Corp e não é coberto por essas licenças.", + "foot_trademark": "TigerTag é uma marca da TigerTag Corp. Política de marca", + "foot_source": "Código-fonte desta página", + "foot_issues": "Relatar um problema na documentação", + "repo": "Repositório", + "open": "Abrir" +} diff --git a/site/i18n/ru.json b/site/i18n/ru.json new file mode 100644 index 0000000..c6884ce --- /dev/null +++ b/site/i18n/ru.json @@ -0,0 +1,125 @@ +{ + "lang_name": "Русский", + "skip": "Перейти к содержимому", + "theme_label": "Светлая или тёмная тема", + "lang_label": "Выберите язык", + "meta_title": "Интеграция TigerTag с Firebase: свой клиент", + "meta_description": "Справочник для клиентов Home Assistant, ESP32, Python и Spoolman, которые читают и обновляют катушки филамента TigerTag через Firebase Auth и Firestore.", + "og_title": "Руководство по интеграции TigerTag с Firebase", + "og_description": "Вход через собственный аккаунт TigerTag пользователя, чтение его катушек, обновление веса: справочник для сторонних клиентов с примерами.", + "og_alt": "Интеграция TigerTag с Firebase: справочник для клиентов Home Assistant, ESP32, Python и Spoolman", + "nav_how": "Как подключиться", + "nav_integrations": "Интеграции", + "nav_security": "Безопасность", + "nav_contract": "Контракт", + "nav_docs": "Документация", + "nav_faq": "Вопросы", + "hero_kicker": "Справочник по сторонним интеграциям", + "hero_headline": "Подключите Home Assistant, весы на ESP32 или скрипт на Python к аккаунту TigerTag.", + "hero_lead": "Этот репозиторий описывает, как любой клиент входит с собственными учётными данными TigerTag пользователя, читает его катушки филамента и стеллажи в Firestore и записывает обратно вес катушек. Здесь описано именно то, что развёрнуто, и каждое изменение фиксируется в журнале изменений.", + "cta_start": "Начать интеграцию", + "cta_examples": "Смотреть примеры", + "cta_changelog": "Читать журнал изменений", + "flow_alt": "Как подключается клиент: клиент входит в Firebase Authentication с адресом электронной почты и паролем пользователя, отправляет полученный idToken в Firestore, а Firestore Security Rules решают, что ему разрешено читать или записывать в users/{uid}/inventory.", + "flow_client": "Ваш клиент", + "flow_auth": "Собственный аккаунт пользователя, idToken на 1 час", + "flow_rules": "Владельцы читают и пишут, друзья читают", + "fact1_value": "0.1.3", + "fact1_label": "Последняя выпущенная версия контракта (CHANGELOG, 2 мая 2026 г.)", + "fact2_value": "1 ч", + "fact2_label": "Срок действия idToken: обновляйте его до истечения", + "fact3_value": "5 мин", + "fact3_label": "Рекомендуемый минимальный интервал опроса", + "fact4_value": "3 месяца", + "fact4_label": "Минимальный срок предупреждения перед переименованием или удалением поля", + "how_title": "Как клиент подключается к TigerTag", + "how_lead": "Каждый клиент выполняет одни и те же четыре шага: получает публичную конфигурацию Firebase, входит от имени пользователя, вызывает Firestore REST API с токеном пользователя и поддерживает этот токен актуальным.", + "step1_title": "Получите публичную конфигурацию", + "step1_text": "Один GET-запрос возвращает конфигурацию проекта Firebase. apiKey публичен по замыслу: он идентифицирует проект, а не вызывающую сторону. Кэшируйте его; обновления раз в неделю достаточно.", + "step2_title": "Войдите от имени пользователя", + "step2_text": "Собственные адрес электронной почты и пароль пользователя возвращают UID, idToken на 1 час и refreshToken. Google Sign-In требует браузера, поэтому клиенты без интерфейса используют пароль.", + "step3_title": "Чтение и запись в Firestore", + "step3_text": "Передавайте idToken как Bearer-токен. Катушки хранятся в users/{uid}/inventory, стеллажи — в users/{uid}/racks; вес указывается нетто в граммах, даты — в миллисекундах Unix.", + "step4_title": "Поддерживайте токен актуальным", + "step4_text": "Обновляйте idToken до истечения часа и заменяйте оба сохранённых токена. При ошибке permission-denied обновите токен один раз и один раз повторите чтение.", + "step4_link": "Обновление idToken", + "how_code_title": "Три вызова из документации", + "how_code_note": "Значения в фигурных скобках берутся из предыдущего вызова: apiKey и projectId — из конфигурации, uid и idToken — из входа.", + "int_title": "Интеграции, описанные в этом репозитории", + "int_lead": "Каждое руководство в docs/clients охватывает аутентификацию, читаемые или записываемые данные, сопоставление полей и код. Приведённые ниже фрагменты скопированы из руководств.", + "ha_pill": "Набросок компонента", + "ha_text": "Пользовательский компонент Home Assistant, который создаёт по одному датчику на каждую катушку, а его состоянием служит оставшийся вес в граммах. Руководство также охватывает инвентарь друзей, постраничную выборку и хранение токенов; config flow и датчик — лишь заготовки, а полностью рабочий компонент ещё предстоит написать.", + "ha_sketch": "Набросок компонента (раздел 9)", + "esp_pill": "Скетч Arduino", + "esp_text": "Для весов или NFC-устройства: найти катушку по считанному RFID UID (сначала uid, затем twin_uid), вычислить вес нетто и записать weight_available и last_update с маской обновления. Руководство завершается полным скетчем для ESP32 (WiFi, ArduinoJson).", + "py_pill": "Полный клиент", + "py_text": "Минимальный клиент на Python на основе Pyrebase4: он входит в аккаунт, выводит список ваших катушек и показывает, сколько катушек у каждого друга. Руководство содержит весь скрипт и пример запуска.", + "sm_pill": "Полный скрипт", + "sm_text": "Односторонний мост из TigerTag в экземпляр Spoolman: бренды становятся поставщиками, каждое сочетание бренда, материала и цвета становится филаментом, а каждый документ инвентаря — катушкой, которая создаётся или обновляется через REST API Spoolman. Запускайте его из cron, systemd или Docker.", + "scale_pill": "Контракт устройства", + "scale_text": "Контракт, которому следуют TigerScale или совместимые весы, чтобы отображаться в сети в Tiger Studio Manager и мобильном приложении TigerTag: документ heartbeat каждые 30 секунд, обновление веса при каждом стабильном показании и парные метки, записываемые одним атомарным пакетом.", + "read_guide": "Читать руководство", + "example_folder": "Папка с примером", + "int_note": "Разрабатываете для другой платформы? Новое руководство помещается в docs/clients, а необязательный рабочий пример — в examples/.", + "sec_title": "Аккаунт пользователя, правила пользователя", + "sec_lead": "Нет общего ключа, который нужно защищать, и нет мастер-токена, который может утечь: клиент может делать ровно то, что может его пользователь, и Firestore обеспечивает это на сервере.", + "sec1_title": "Без сервисного аккаунта", + "sec1_text": "Каждый клиент входит с собственными учётными данными конечного пользователя. Никогда не включайте firebase-admin SDK или какой-либо административный ключ в сторонний клиент.", + "sec2_title": "Решают Security Rules", + "sec2_text": "Firestore Security Rules проверяют каждый запрос по request.auth.uid: владельцы читают и записывают собственные документы, друзья могут читать инвентарь и стеллажи, а всё, что не разрешено, запрещено. Публичная копия правил находится в rules/firestore.rules.", + "sec3_title": "Одна связь на каждую дружбу", + "sec3_text": "Принятие запроса в друзья записывает документ с каждой стороны. Пока существует users/{friend}/friends/{you}, вы можете читать инвентарь и стеллажи этого друга только для чтения; удаление документа немедленно прекращает доступ.", + "sec4_title": "Вежливый опрос", + "sec4_text": "Опрашивайте не чаще чем раз в 5 минут и никогда не чаще чем раз в 30 секунд, записывайте только при изменении значения и передавайте limit при чтении списков (200 катушек, 50 стеллажей). Firebase App Check не применяется принудительно.", + "contract_title": "Публичный контракт для интеграторов", + "contract_lead": "Репозиторий считается публичным контрактом: изменения, затрагивающие клиенты, попадают в этот репозиторий в том же выпуске, а журнал изменений относит каждое из них к категории Added, Changed, Deprecated, Removed, Fixed или Security.", + "stable_title": "Не изменится без предупреждения", + "stable1": "Пути к документам, такие как users/{uid}/inventory/{spoolId}, users/{uid}/racks/{rackId} и users/{uid}/friends/{friendUid}.", + "stable2": "Имя и значение каждого описанного поля: weight_available — вес нетто в граммах, last_update — миллисекунды Unix, deleted: true означает мягкое удаление.", + "stable3": "Модель доступа друзей и конечные точки Firebase Auth, через которые входит клиент.", + "stable4": "apiKey и projectId, если только весь проект не переедет, о чём будет объявлено не менее чем за 6 месяцев.", + "may_title": "Может измениться: будьте осторожны", + "may1": "В справочные таблицы добавляются новые бренды и материалы; числовые id только добавляются и никогда не используются повторно.", + "may2": "Вспомогательные HTTPS-эндпоинты на cdn.tigertag.io могут менять структуру запросов и ответов.", + "may3": "Новые коллекции и новые поля всегда только добавляются: игнорируйте неизвестные поля и сохраняйте совместимость.", + "contract_link": "Гарантии стабильности в README", + "docs_title": "Справочная документация", + "docs_lead": "Основные документы, на которых строится любая интеграция, в том порядке, в каком они нужны клиенту.", + "doc1": "Публичный эндпоинт конфигурации Firebase, назначение каждого поля и срок кэширования.", + "doc2": "Вход по адресу электронной почты и паролю, обновление токена и схема повтора при permission-denied.", + "doc3": "Все коллекции и поля Firestore: инвентарь, стеллажи, друзья, профили, весы.", + "doc4": "Как работает доступ друзей: публичные коды, закрытые ключи, двусторонняя связь.", + "doc5": "Интервалы опроса, ограничения запросов, мониторинг и почему App Check не применяется принудительно.", + "doc6": "Публичная копия развёрнутых Firestore Security Rules.", + "eco_title": "Часть экосистемы TigerTag", + "eco_lead": "Это руководство посвящено облачному аккаунту. У чипов, приложений и SDK есть собственные репозитории.", + "eco_proto_title": "Протокол TigerTag", + "eco_proto_text": "Открытый формат NFC-чипов для катушек филамента и флаконов смолы, его спецификация и справочная база данных.", + "eco_studio_text": "Настольное приложение (Electron), которое управляет тем же инвентарём; документация сверяется с его выпусками.", + "eco_sdk_title": "SDK TigerTag", + "eco_sdk_text": "Чтение, проверка, создание и изменение чипов TigerTag офлайн на JavaScript или Python, под лицензией Apache-2.0.", + "faq_title": "Вопросы", + "faq1_q": "Нужен ли мне сервисный аккаунт или административный ключ?", + "faq1_a": "Нет. Каждый клиент входит с собственными адресом электронной почты и паролем TigerTag конечного пользователя, а Firestore Security Rules решают, что этому пользователю разрешено читать или записывать. Общего административного ключа нет, и firebase-admin SDK никогда не должен использоваться в стороннем клиенте.", + "faq2_q": "Почему apiKey Firebase публичен?", + "faq2_a": "apiKey идентифицирует проект Firebase TigerTag, а не вызывающую сторону. Без действительного idToken, полученного при входе от имени реального пользователя, он не даёт доступа ни к чему; контроль доступа обеспечивается аутентификацией и Security Rules.", + "faq3_q": "Может ли Home Assistant или ESP32 входить через Google?", + "faq3_a": "Не напрямую: Google Sign-In требует перенаправления в браузере. Пользователь, который всегда входил только через Google, сначала задаёт пароль для аккаунта через функцию восстановления пароля в настольном или мобильном приложении, а затем входит по адресу электронной почты и паролю.", + "faq4_q": "Как часто моя интеграция может выполнять опрос?", + "faq4_a": "Не чаще чем раз в 5 минут для датчика, раз в 15 минут для обзора стеллажей и никогда не чаще чем раз в 30 секунд. Весы записывают данные только при изменении веса. При чтении списков следует передавать limit: 200 для инвентаря, 50 для стеллажей.", + "faq5_q": "Может ли моя интеграция читать катушки друга?", + "faq5_a": "Да, только для чтения. Когда два пользователя дружат, у каждого есть документ о другом в users/{uid}/friends, и правила позволяют другу читать инвентарь и стеллажи. Любой из пользователей может в любой момент прекратить дружбу, что немедленно прекращает доступ.", + "faq6_q": "Что делать, если рабочая среда расходится с этой документацией?", + "faq6_a": "Приоритет у документации: расхождение означает, что в выпуск попала регрессия. Откройте issue в репозитории, указав страницу, путь, который вы читали или записывали, и ответ, удалив токены и закрытые ключи.", + "faq7_q": "Под какой лицензией можно повторно использовать код?", + "faq7_a": "Security Rules и все фрагменты кода в документации распространяются под лицензией Apache-2.0, а для фрагментов, вставленных в ваш собственный проект, указание авторства не требуется. Сама документация распространяется под лицензией CC-BY-4.0. Название, логотип и сервис TigerTag на эти лицензии не распространяются.", + "join_title": "Начните свою интеграцию", + "join_text": "Выберите руководство для своей платформы в быстрых ссылках README и следуйте ему от входа до первого чтения.", + "join_btn": "Открыть быстрые ссылки", + "foot_licences": "Документация под лицензией CC-BY-4.0, код и фрагменты кода под лицензией Apache-2.0. Сервис TigerTag управляется TigerTag Corp и на эти лицензии не распространяется.", + "foot_trademark": "TigerTag — товарный знак TigerTag Corp. Политика использования товарного знака", + "foot_source": "Исходный код этой страницы", + "foot_issues": "Сообщить о проблеме в документации", + "repo": "Репозиторий", + "open": "Открыть" +} diff --git a/site/i18n/zh.json b/site/i18n/zh.json new file mode 100644 index 0000000..9f82a94 --- /dev/null +++ b/site/i18n/zh.json @@ -0,0 +1,125 @@ +{ + "lang_name": "中文", + "skip": "跳至正文", + "theme_label": "浅色或深色", + "lang_label": "选择语言", + "meta_title": "TigerTag Firebase 集成:接入您自己的客户端", + "meta_description": "面向 Home Assistant、ESP32、Python 和 Spoolman 客户端的参考文档,介绍如何通过 Firebase Auth 和 Firestore 读取并更新 TigerTag 线材卷。", + "og_title": "TigerTag Firebase 集成指南", + "og_description": "使用用户自己的 TigerTag 账户登录,读取其线材卷,更新重量:面向第三方客户端的参考文档,附带示例。", + "og_alt": "TigerTag Firebase 集成:Home Assistant、ESP32、Python 和 Spoolman 客户端参考文档", + "nav_how": "连接方式", + "nav_integrations": "集成", + "nav_security": "安全", + "nav_contract": "契约", + "nav_docs": "文档", + "nav_faq": "常见问题", + "hero_kicker": "第三方集成参考", + "hero_headline": "将 Home Assistant、ESP32 电子秤或 Python 脚本连接到 TigerTag 账户。", + "hero_lead": "本仓库说明任何客户端如何使用用户自己的 TigerTag 凭据登录,读取其在 Firestore 中的线材卷和料架,并写回线材卷重量。这里记录的内容即已部署的内容,每一项变更都会记入更新日志。", + "cta_start": "开始集成", + "cta_examples": "浏览示例", + "cta_changelog": "阅读更新日志", + "flow_alt": "客户端的连接方式:客户端使用用户的邮箱和密码登录 Firebase Authentication,将返回的 idToken 发送给 Firestore,由 Firestore Security Rules 决定它可以读取或写入 users/{uid}/inventory 中的哪些内容。", + "flow_client": "您的客户端", + "flow_auth": "用户自己的账户,有效期 1 小时的 idToken", + "flow_rules": "所有者可读写,好友可读", + "fact1_value": "0.1.3", + "fact1_label": "最新发布的契约版本(CHANGELOG,2026 年 5 月 2 日)", + "fact2_value": "1 小时", + "fact2_label": "idToken 的有效期:请在过期前刷新", + "fact3_value": "5 分钟", + "fact3_label": "建议的最短轮询间隔", + "fact4_value": "3 个月", + "fact4_label": "字段重命名或移除前的最短弃用通知期", + "how_title": "客户端如何连接 TigerTag", + "how_lead": "每个客户端都遵循相同的四个步骤:获取公开的 Firebase 配置,以用户身份登录,使用用户的令牌调用 Firestore REST API,并保持该令牌有效。", + "step1_title": "获取公开配置", + "step1_text": "一次 GET 请求即可返回 Firebase 项目配置。apiKey 本来就是公开的:它标识的是项目,而不是调用方。请缓存它,每周刷新一次即可。", + "step2_title": "以用户身份登录", + "step2_text": "用户自己的邮箱和密码会返回一个 UID、一个有效期 1 小时的 idToken 和一个 refreshToken。Google Sign-In 需要浏览器,因此无界面客户端使用密码登录。", + "step3_title": "读写 Firestore", + "step3_text": "将 idToken 作为 Bearer 令牌发送。线材卷位于 users/{uid}/inventory,料架位于 users/{uid}/racks;重量为净重(克),日期为 Unix 毫秒时间戳。", + "step4_title": "保持令牌有效", + "step4_text": "在一小时到期前刷新 idToken,并替换已保存的两个令牌。遇到 permission-denied 时,刷新一次并重试一次读取。", + "step4_link": "刷新 idToken", + "how_code_title": "三次调用,摘自文档", + "how_code_note": "花括号中的值来自上一次调用:apiKey 和 projectId 来自配置,uid 和 idToken 来自登录。", + "int_title": "本仓库中记录的集成", + "int_lead": "docs/clients 中的每份指南都涵盖身份验证、读取或写入的数据、字段映射和代码。以下代码片段摘自这些指南。", + "ha_pill": "组件草稿", + "ha_text": "一个 Home Assistant 自定义组件,为每个线材卷提供一个传感器,状态值为剩余重量(克)。指南还涵盖好友的库存、分页和令牌存储;配置流程和传感器只是骨架代码,完整可运行的组件尚待编写。", + "ha_sketch": "组件草稿(第 9 节)", + "esp_pill": "Arduino 程序", + "esp_text": "适用于电子秤或 NFC 设备:根据扫描到的 RFID UID 查找线材卷(先 uid,再 twin_uid),计算净重,并通过更新掩码写入 weight_available 和 last_update。指南最后附有一个完整的 ESP32 程序(WiFi、ArduinoJson)。", + "py_pill": "完整客户端", + "py_text": "一个基于 Pyrebase4 的极简 Python 客户端:登录,列出您的线材卷,并显示每位好友拥有的线材卷数量。指南包含完整脚本和一次运行示例。", + "sm_pill": "完整脚本", + "sm_text": "从 TigerTag 到 Spoolman 实例的单向桥接:品牌成为供应商,每种品牌、材料和颜色的组合成为一种线材,每个库存文档成为一个线材卷,通过 Spoolman 的 REST API 写入或更新。可通过 cron、systemd 或 Docker 运行。", + "scale_pill": "设备契约", + "scale_text": "TigerScale 或兼容电子秤要在 Tiger Studio Manager 和 TigerTag 移动应用中显示为在线所需遵循的契约:每 30 秒一个心跳文档,每次读数稳定时更新一次重量,双芯片标签在一个原子批处理中写入。", + "read_guide": "阅读指南", + "example_folder": "示例文件夹", + "int_note": "要为其他平台开发?新指南放在 docs/clients 中,可选的可运行示例放在 examples/ 中。", + "sec_title": "用户的账户,用户的规则", + "sec_lead": "没有需要保护的共享密钥,也没有可能泄露的主令牌:客户端能做的恰好就是其用户能做的,并由 Firestore 在服务器端强制执行。", + "sec1_title": "没有服务账户", + "sec1_text": "每个客户端都使用最终用户自己的凭据登录。切勿在第三方客户端中包含 firebase-admin SDK 或任何管理员密钥。", + "sec2_title": "由 Security Rules 决定", + "sec2_text": "Firestore Security Rules 根据 request.auth.uid 检查每个请求:所有者可读写自己的文档,好友可读取库存和料架,任何未被允许的操作都会被拒绝。规则的公开副本位于 rules/firestore.rules。", + "sec3_title": "每段好友关系一条纽带", + "sec3_text": "接受好友请求会在双方各写入一个文档。只要 users/{friend}/friends/{you} 存在,您就可以只读访问该好友的库存和料架;删除该文档会立即切断访问。", + "sec4_title": "礼貌轮询", + "sec4_text": "轮询频率最高每 5 分钟一次,绝不低于 30 秒,仅在值发生变化时写入,并在列表读取时传入 limit(200 个线材卷,50 个料架)。未强制启用 Firebase App Check。", + "contract_title": "面向集成者的公开契约", + "contract_lead": "本仓库被视为一份公开契约:影响客户端的变更会在同一版本中同步到本仓库,更新日志将每项变更记录为 Added、Changed、Deprecated、Removed、Fixed 或 Security。", + "stable_title": "不会在未经通知的情况下变更", + "stable1": "文档路径,例如 users/{uid}/inventory/{spoolId}、users/{uid}/racks/{rackId} 和 users/{uid}/friends/{friendUid}。", + "stable2": "每个已记录字段的名称和含义:weight_available 为净重(克),last_update 为 Unix 毫秒时间戳,deleted: true 表示软删除。", + "stable3": "好友访问模型,以及客户端登录所用的 Firebase Auth 端点。", + "stable4": "apiKey 和 projectId,除非整个项目迁移,且会至少提前 6 个月公布。", + "may_title": "可能变更:请做好防御", + "may1": "查找表会新增品牌和材料;数字 id 只增不减,且永不重复使用。", + "may2": "cdn.tigertag.io 上的便捷 HTTPS 端点,其请求和响应结构可能会演变。", + "may3": "新集合和新字段始终是增量添加的:忽略您不认识的字段,即可保持兼容。", + "contract_link": "README 中的稳定性保证", + "docs_title": "参考文档", + "docs_lead": "每个集成所依赖的核心文档,按客户端需要的顺序排列。", + "doc1": "公开的 Firebase 配置端点、每个字段的用途,以及缓存时长。", + "doc2": "邮箱和密码登录、令牌刷新,以及 permission-denied 重试模式。", + "doc3": "每个 Firestore 集合和字段:库存、料架、好友、个人资料、电子秤。", + "doc4": "好友访问的工作原理:公开代码、私有密钥、双向纽带。", + "doc5": "轮询间隔、查询限制、监控,以及为何未强制启用 App Check。", + "doc6": "已部署的 Firestore Security Rules 的公开副本。", + "eco_title": "TigerTag 生态的一部分", + "eco_lead": "本指南涵盖云端账户。芯片、应用和 SDK 各有自己的代码仓库。", + "eco_proto_title": "TigerTag 协议", + "eco_proto_text": "用于线材卷和树脂瓶的开放 NFC 芯片格式、其规范和参考数据库。", + "eco_studio_text": "管理同一库存的桌面应用(Electron);本文档会根据其各个版本进行核对。", + "eco_sdk_title": "TigerTag SDK", + "eco_sdk_text": "离线读取、验证、创建和修补 TigerTag 芯片,支持 JavaScript 或 Python,采用 Apache-2.0 许可。", + "faq_title": "常见问题", + "faq1_q": "我需要服务账户或管理员密钥吗?", + "faq1_a": "不需要。每个客户端都使用最终用户自己的 TigerTag 邮箱和密码登录,由 Firestore Security Rules 决定该用户可以读取或写入什么。不存在共享的管理员密钥,并且绝不能在第三方客户端中使用 firebase-admin SDK。", + "faq2_q": "为什么 Firebase apiKey 是公开的?", + "faq2_a": "apiKey 标识的是 TigerTag Firebase 项目,而不是调用方。如果没有以真实用户身份登录获得的有效 idToken,它无法访问任何内容;访问控制由身份验证和 Security Rules 完成。", + "faq3_q": "Home Assistant 或 ESP32 可以使用 Google 登录吗?", + "faq3_a": "不能直接登录:Google Sign-In 需要浏览器重定向。只使用过 Google 登录的用户需要先通过桌面或移动应用的忘记密码流程为账户设置密码,然后再使用邮箱和密码登录。", + "faq4_q": "我的集成可以多频繁地轮询?", + "faq4_a": "传感器最高每 5 分钟一次,料架概览每 15 分钟一次,且绝不低于 30 秒。电子秤仅在重量变化时写入。列表读取应传入 limit:库存为 200,料架为 50。", + "faq5_q": "我的集成可以读取好友的线材卷吗?", + "faq5_a": "可以,只读。两位用户成为好友后,双方都会在 users/{uid}/friends 下拥有对方的文档,规则允许好友读取库存和料架。任何一方都可以随时解除好友关系,访问会立即被切断。", + "faq6_q": "如果生产环境与本文档不一致怎么办?", + "faq6_a": "以文档为准:出现差异意味着发布了回归问题。请在代码仓库中提交 issue,附上页面、您读取或写入的路径以及响应内容,并删除其中的令牌和私有密钥。", + "faq7_q": "我可以在什么许可下复用这些代码?", + "faq7_a": "Security Rules 和文档中的每个代码片段均采用 Apache-2.0 许可,粘贴到您自己项目中的代码片段无需署名。文档本身采用 CC-BY-4.0 许可。TigerTag 名称、标志和服务不在这些许可范围内。", + "join_title": "开始您的集成", + "join_text": "在 README 的快速链接中选择适合您平台的指南,从登录一直跟随到第一次读取。", + "join_btn": "打开快速链接", + "foot_licences": "文档采用 CC-BY-4.0 许可,代码和代码片段采用 Apache-2.0 许可。TigerTag 服务由 TigerTag Corp 运营,不在这些许可范围内。", + "foot_trademark": "TigerTag 是 TigerTag Corp 的商标。商标政策", + "foot_source": "本页源代码", + "foot_issues": "报告文档问题", + "repo": "代码仓库", + "open": "打开" +} diff --git a/site/og.jpg b/site/og.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a1503bfbf85d7f49168f0f878f6667e0255daad4 GIT binary patch literal 65493 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 fZyo%vTO@1$(M hard: + errors.append(f"{where}: {label} is {len(value)} characters (over {hard}): {value!r}") + elif len(value) > hi: + warnings.append(f"{where}: {label} is {len(value)} characters (results show ~{hi})") + elif len(value) < lo and HTML_LANG.get(lang, lang) != "zh-Hans": + warnings.append(f"{where}: {label} is only {len(value)} characters") + titles.setdefault(title, []).append(lang) + descs.setdefault(desc, []).append(lang) + + # One h1: the page's single subject. + if p.h1 != 1: + errors.append(f"{where}: {p.h1}

    (exactly one expected)") + + # Canonical and hreflang: every language points to every language, and x-default. + canon = [l.get("href") for l in p.links if l.get("rel") == "canonical"] + if canon != [SITE_URL + page_path(lang)]: + errors.append(f"{where}: canonical is {canon}, expected {SITE_URL + page_path(lang)}") + alts = {l.get("hreflang"): l.get("href") for l in p.links if l.get("rel") == "alternate" and l.get("hreflang")} + if alts != expected_alternates: + missing = sorted(set(expected_alternates) - set(alts)) + wrong = sorted(k for k in alts if alts[k] != expected_alternates.get(k)) + errors.append(f"{where}: hreflang set incomplete or wrong (missing {missing}, wrong {wrong})") + + # Sharing cards. + for prop in ("og:title", "og:description", "og:image", "og:image:alt", "og:url", "twitter:card"): + if not (p.meta.get(prop) or [""])[0]: + errors.append(f"{where}: no {prop}") + + # Images: an alt attribute on every one (empty only for a decorative duplicate). + for img in p.imgs: + if "alt" not in img: + errors.append(f"{where}: has no alt attribute") + # The hero diagram is HTML, not a picture: it carries its description itself. + if not p.hero_label: + errors.append(f"{where}: the hero diagram (figure.flow) has no aria-label") + + # Structured data: valid JSON, the expected types, the FAQ the reader sees. + types = [] + for raw in p.ld: + try: + data = json.loads(raw) + except ValueError as e: + errors.append(f"{where}: JSON-LD does not parse ({e})") + continue + types.append(data.get("@type")) + if data.get("@type") == "FAQPage" and len(data.get("mainEntity", [])) != p.faq_items: + errors.append(f"{where}: FAQPage lists {len(data.get('mainEntity', []))} questions, the page shows {p.faq_items}") + missing_ld = REQUIRED_LD - set(types) + if missing_ld: + errors.append(f"{where}: JSON-LD missing {sorted(missing_ld)}") + + # In-page links land somewhere. + for href in p.hrefs: + if href.startswith("#") and len(href) > 1 and href[1:] not in p.ids: + errors.append(f"{where}: link {href} has no target") + + # No emoji anywhere on the page: inline SVG icons or words. + if EMOJI.search(path.read_text(encoding="utf-8")): + errors.append(f"{where}: contains an emoji") + + # The machine-readable description is advertised. + if not any(l.get("rel") == "alternate" and (l.get("href") or "").endswith("llms.txt") for l in p.links): + warnings.append(f"{where}: no to llms.txt") + + for value, langs in list(titles.items()) + list(descs.items()): + if value and len(langs) > 1: + errors.append(f"same title or description in {', '.join(langs)}: {value!r} (an untranslated page?)") + + sitemap = (out / "sitemap.xml").read_text(encoding="utf-8") if (out / "sitemap.xml").exists() else "" + for lang in LANGS: + if f"{SITE_URL + page_path(lang)}" not in sitemap: + errors.append(f"sitemap.xml: {SITE_URL + page_path(lang)} missing") + if "" not in sitemap: + warnings.append("sitemap.xml: no ") + robots = (out / "robots.txt").read_text(encoding="utf-8") if (out / "robots.txt").exists() else "" + if f"Sitemap: {SITE_URL}sitemap.xml" not in robots: + errors.append("robots.txt: no Sitemap line") + if re.search(r"(?m)^Disallow: /\s*$", robots): + errors.append("robots.txt: disallows the whole site") + if not (out / "llms.txt").exists(): + errors.append("llms.txt missing at the site root") + return errors, warnings + + +def main(): + out = Path(sys.argv[1] if len(sys.argv) > 1 else "_site") + errors, warnings = check(out) + for w in warnings: + print(f"warning: {w}") + for e in errors: + print(f"error: {e}") + print(f"seo_check: {len(LANGS)} pages, {len(errors)} error(s), {len(warnings)} warning(s)") + sys.exit(1 if errors else 0) + + +if __name__ == "__main__": + main() diff --git a/site/site.css b/site/site.css new file mode 100644 index 0000000..8cc8efe --- /dev/null +++ b/site/site.css @@ -0,0 +1,204 @@ +/* The TigerTag Firebase Integration site — same look as the TigerTag protocol site (dark by default, + light when the system asks for it or when chosen with the button). */ +:root { + --bg: #14161a; --card: #1e2128; --line: #2b2f38; --text: #f2f2f2; + --muted: #9aa0aa; --accent: #f58220; --link: #f58220; --code: #101216; + --shadow: rgba(0, 0, 0, .45); + color-scheme: dark; +} +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --bg: #f4f5f7; --card: #ffffff; --line: #dde0e6; --text: #16181d; + --muted: #5d6470; --accent: #e06d0c; --link: #b04a08; --code: #f0f1f4; + --shadow: rgba(0, 0, 0, .12); + color-scheme: light; + } +} +:root[data-theme="light"] { + --bg: #f4f5f7; --card: #ffffff; --line: #dde0e6; --text: #16181d; + --muted: #5d6470; --accent: #e06d0c; --link: #b04a08; --code: #f0f1f4; + --shadow: rgba(0, 0, 0, .12); + color-scheme: light; +} + +* { box-sizing: border-box; } +html { scroll-behavior: smooth; overflow-x: clip; } +body { + margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); + font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + /* The hero picture overflows its column on purpose: never let it scroll the page sideways. */ + overflow-x: clip; +} +a { color: var(--link); } +img { max-width: 100%; height: auto; } +.wrap { width: min(1200px, 100% - 48px); margin-left: auto; margin-right: auto; } +.skip { position: absolute; left: -999px; top: 8px; background: var(--card); padding: 8px 12px; border-radius: 8px; } +.skip:focus { left: 8px; z-index: 10; } +.muted, p { color: var(--muted); } +p { margin: 0 0 14px; } +b { color: var(--text); } +h1, h2, h3 { line-height: 1.2; color: var(--text); } +h2 { font-size: clamp(1.5rem, 1.1rem + 1.4vw, 2.2rem); margin: 0 0 8px; letter-spacing: -.01em; } +h3 { font-size: 1.08rem; margin: 0 0 8px; } +.sub { font-size: 1.05rem; max-width: 760px; margin-bottom: 22px; } +.gap { margin-top: 48px; } +section { margin-top: 64px; scroll-margin-top: 80px; } + +/* Header: the logo, the sections, the theme and the language. */ +header.bar { position: sticky; top: 0; z-index: 5; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); } +header.bar .wrap { display: flex; align-items: center; gap: 18px; min-height: 64px; } +.brandmark { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--text); font-weight: 800; font-size: 1.2rem; letter-spacing: -.01em; } +.brandmark b { color: var(--accent); } +.brandmark img { height: 36px; width: auto; } +.logo-light { display: none; } +:root[data-shown="light"] .logo-light { display: block; } +:root[data-shown="light"] .logo-dark { display: none; } +.nav { display: flex; gap: 18px; flex: 1; justify-content: center; flex-wrap: wrap; } +/* Longer words (French, German, Russian…) keep the menu on one line. */ +@media (max-width: 1320px) { .nav { gap: 12px; } .nav a { font-size: .88rem; } } +.nav a { color: var(--muted); text-decoration: none; font-size: .95rem; } +.nav a:hover { color: var(--text); } +.tools { display: flex; gap: 8px; align-items: center; } +.themebtn, .langbtn { + display: flex; align-items: center; gap: 6px; height: 40px; cursor: pointer; color: var(--muted); + background: var(--bg); border: 1px solid var(--line); border-radius: 10px; font: inherit; font-size: .9rem; +} +.themebtn { width: 40px; justify-content: center; } +.langbtn { padding: 0 12px; list-style: none; } +.langbtn::-webkit-details-marker { display: none; } +.themebtn:hover, .langbtn:hover { border-color: var(--accent); color: var(--text); } +.themebtn svg, .langbtn svg { width: 17px; height: 17px; flex: none; } +.themebtn .sun { display: none; } +:root[data-shown="light"] .themebtn .sun { display: block; } +:root[data-shown="light"] .themebtn .moon { display: none; } +.langbox { position: relative; } +.langlist { + position: absolute; right: 0; top: 46px; margin: 0; padding: 6px; list-style: none; min-width: 180px; + background: var(--card); border: 1px solid var(--line); border-radius: 12px; box-shadow: 0 12px 30px var(--shadow); +} +.langlist a { display: block; padding: 7px 10px; border-radius: 8px; color: var(--text); text-decoration: none; } +.langlist a:hover, .langlist a[aria-current="true"] { background: var(--bg); color: var(--accent); } + +/* Hero. */ +.hero { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); gap: 32px; align-items: center; margin-top: 32px; } +h1 { margin: 0; } +h1 .kicker, h1 .name { display: block; } +.kicker { color: var(--accent); font-weight: 650; text-transform: uppercase; letter-spacing: .08em; font-size: .85rem; margin-bottom: 10px; } +.hero .name { margin: 0; font-size: clamp(3rem, 1.8rem + 5vw, 5.4rem); font-weight: 800; letter-spacing: -.02em; line-height: 1; } +.hero .name span { color: var(--accent); } +.headline { text-wrap: balance; margin: 16px 0 12px; color: var(--text); font-weight: 650; line-height: 1.25; font-size: clamp(1.25rem, 1rem + 1.1vw, 1.8rem); } +.lead { font-size: 1.08rem; } +.ctas { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; } +.btn { + display: inline-flex; align-items: center; padding: 12px 20px; border-radius: 999px; text-decoration: none; + border: 1px solid var(--line); color: var(--text); background: var(--card); font-weight: 600; +} +.btn:hover { border-color: var(--accent); } +.btn.primary { background: var(--accent); border-color: var(--accent); color: #1a0f05; } +.btn.small { padding: 8px 14px; font-size: .9rem; } + +/* Four facts. */ +.facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; margin-top: 36px; } +.fact { background: var(--card); border: 1px solid var(--line); border-radius: 16px; padding: 18px 20px; } +.fact b { display: block; font-size: 1.6rem; color: var(--accent); line-height: 1.2; } +.fact span { color: var(--muted); font-size: .93rem; } + +/* Cards and grids. */ +.card { background: var(--card); border: 1px solid var(--line); border-radius: 18px; padding: 24px; } +.card p:last-child { margin-bottom: 0; } +.card.accent { border-color: var(--accent); } +.grid3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; } +.ico { width: 28px; height: 28px; fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; margin-bottom: 10px; } +.pill { display: inline-block; font: 600 .78rem ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--accent); border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; margin-bottom: 12px; } +.more { display: inline-block; font-weight: 600; text-decoration: none; margin-top: 4px; } +.more:hover { text-decoration: underline; } +.links { display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: center; } +pre { margin: 0 0 14px; background: var(--code); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; overflow-x: auto; } +code { font: .95rem ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text); } + +/* How it works. */ +.steps { list-style: none; padding: 0; margin: 0 0 16px; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; } +.num { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; background: var(--accent); color: #1a0f05; font-weight: 800; margin-bottom: 12px; } + +/* FAQ. */ +.faq { display: grid; gap: 10px; } +.faq details { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 4px 20px; } +.faq summary { cursor: pointer; font-weight: 650; padding: 12px 0; color: var(--text); } +.faq details p { padding-bottom: 6px; } + +/* The call at the end. */ +.join { + display: flex; align-items: center; justify-content: space-between; gap: 24px; + padding: 30px 32px; border-radius: 18px; color: #fff; + background: linear-gradient(120deg, #c2410c 0%, #b03a12 55%, #9a2f12 100%); +} +.join h2, .join p { color: #fff; margin: 0 0 6px; } +.joinbtn { flex: none; padding: 14px 26px; border-radius: 999px; text-decoration: none; background: #fff; color: #9a2f12; font-weight: 700; white-space: nowrap; } +footer.foot { margin-top: 48px; padding: 28px 0 48px; border-top: 1px solid var(--line); text-align: center; font-size: .9rem; } +footer.foot .links { justify-content: center; } + +/* Header wordmark: the product under the TigerTag name. */ +.brandmark small { font-size: .8rem; font-weight: 650; color: var(--muted); margin-left: 2px; letter-spacing: 0; } + +/* Hero: the title and the path every client follows. */ +.hero { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); } +.hero .name { font-size: clamp(2.6rem, 1.6rem + 4vw, 4.6rem); } +.hero .name em { display: block; font-style: normal; color: var(--text); font-size: .5em; font-weight: 750; letter-spacing: -.01em; margin-top: 10px; } +.flow { margin: 0; display: grid; gap: 0; background: var(--card); border: 1px solid var(--line); border-radius: 20px; padding: 22px; box-shadow: 0 24px 60px -30px var(--shadow); } +.node { display: flex; gap: 14px; align-items: center; background: var(--bg); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; } +.node.accent { border-color: var(--accent); } +.node .ico { margin: 0; flex: none; } +.node b { display: block; } +.node span { display: block; color: var(--muted); font-size: .9rem; } +.node code { font-size: .85rem; color: var(--accent); } +.arrow { position: relative; min-height: 34px; display: flex; align-items: center; padding-left: 44px; } +.arrow::before { content: ""; position: absolute; left: 29px; top: 4px; bottom: 4px; border-left: 2px dashed color-mix(in srgb, var(--accent) 60%, var(--line)); } +.arrow::after { content: ""; position: absolute; left: 25px; bottom: 2px; border: 5px solid transparent; border-top-color: var(--accent); border-bottom: 0; } +.arrow code { font-size: .76rem; color: var(--muted); background: none; } + +/* Integrations: one card per client the repository documents. */ +.ints .card { display: flex; flex-direction: column; } +.ints .card pre { margin-top: auto; } +.ints .card .links { margin-top: 2px; } +.ints .scale { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px; align-items: center; } +.ints .scale pre { margin: 0; } +pre code { font-size: .84rem; line-height: 1.55; white-space: pre; } +.note { font-size: .95rem; } +.card--wide { grid-column: 1 / -1; } + +/* Contract lists. */ +.ticks { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; } +.ticks li { position: relative; padding-left: 26px; color: var(--muted); } +.ticks li::before { content: ""; position: absolute; left: 2px; top: .45em; width: 12px; height: 7px; border-left: 2px solid var(--accent); border-bottom: 2px solid var(--accent); transform: rotate(-45deg); } +#contract > .links { margin-top: 4px; } + +/* Reference documents. */ +.docs { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } +.docs li { background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 14px 18px; } +.docs a { font: 700 .95rem ui-monospace, SFMono-Regular, Menlo, monospace; display: block; text-decoration: none; margin-bottom: 4px; } +.docs a:hover { text-decoration: underline; } +.docs span { color: var(--muted); font-size: .95rem; } +.small { font-size: .85rem; } + +/* Tablet. */ +@media (max-width: 999px) { + .nav { display: none; } + header.bar .wrap { justify-content: space-between; } + .hero { grid-template-columns: 1fr; gap: 24px; } + .facts, .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .grid3, .docs { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .ints .scale { grid-template-columns: 1fr; } +} +/* Phone. */ +@media (max-width: 639px) { + .wrap { width: calc(100% - 32px); } + section { margin-top: 44px; } + .facts, .steps, .grid3, .grid2, .docs { grid-template-columns: 1fr; } + .card { padding: 20px; border-radius: 14px; } + .flow { padding: 14px; } + .join { flex-direction: column; align-items: flex-start; padding: 24px 20px; } + .joinbtn { align-self: stretch; text-align: center; } + .langbtn span { display: none; } + .brandmark small { display: none; } +} diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..f1a88ef --- /dev/null +++ b/site/site.js @@ -0,0 +1,45 @@ +/* The theme button and the remembered language. Everything else on the site is + static HTML: each language is its own page, built by site/build.py. */ +(function () { + const root = document.documentElement; + const media = window.matchMedia("(prefers-color-scheme: light)"); + + function shown() { + const chosen = root.dataset.theme; + root.dataset.shown = chosen || (media.matches ? "light" : "dark"); + } + shown(); + media.addEventListener("change", shown); + + const btn = document.getElementById("themebtn"); + if (btn) { + btn.addEventListener("click", function () { + const next = root.dataset.shown === "light" ? "dark" : "light"; + root.dataset.theme = next; + try { localStorage.setItem("tigertag-theme", next); } catch (e) {} + shown(); + }); + } + + /* The language menu closes on a click outside it, or on Escape. */ + const box = document.querySelector(".langbox"); + if (box) { + document.addEventListener("click", function (e) { if (box.open && !box.contains(e.target)) box.open = false; }); + document.addEventListener("keydown", function (e) { if (e.key === "Escape") box.open = false; }); + } + + /* A language picked in the menu is kept for the next visit to the English + root page, which then forwards to it. Search engines read every page as it + is: the redirect only follows a choice the reader made. */ + document.querySelectorAll(".langlist a[data-lang]").forEach(function (a) { + a.addEventListener("click", function () { + try { localStorage.setItem("tigertag-lang", a.dataset.lang); } catch (e) {} + }); + }); + if (root.dataset.lang === "en") { + let saved = null; + try { saved = localStorage.getItem("tigertag-lang"); } catch (e) {} + const target = saved && saved !== "en" && document.querySelector('.langlist a[data-lang="' + saved + '"]'); + if (target) window.location.replace(target.href + window.location.hash); + } +})(); diff --git a/site/template.html b/site/template.html new file mode 100644 index 0000000..cf0c179 --- /dev/null +++ b/site/template.html @@ -0,0 +1,281 @@ + + + + + +{{meta_title}} + + +{{@alternates}} + + + +{{@oglocalealt}} + + + + + + + + + + + + + + + + + + + +{{@jsonld}} + + + +
    +
    + + + + TigerTag Firebase + + +
    + +
    + + + {{lang_name}} + +
      +{{@langmenu}} +
    +
    +
    +
    +
    + +
    +
    +
    +

    {{hero_kicker}}TigerTag Firebase Integration

    +

    {{hero_headline}}

    +

    {{hero_lead}}

    + +
    + +
    + +
    +
    {{fact1_value}}{{fact1_label}}
    +
    {{fact2_value}}{{fact2_label}}
    +
    {{fact3_value}}{{fact3_label}}
    +
    {{fact4_value}}{{fact4_label}}
    +
    + +
    +

    {{how_title}}

    +

    {{how_lead}}

    +
      +
    1. 1

      {{step1_title}}

      {{step1_text}}

      01-firebase-config.md
    2. +
    3. 2

      {{step2_title}}

      {{step2_text}}

      02-authentication.md
    4. +
    5. 3

      {{step3_title}}

      {{step3_text}}

      03-data-model.md
    6. +
    7. 4

      {{step4_title}}

      {{step4_text}}

      {{step4_link}}
    8. +
    +
    +

    {{how_code_title}}

    +
    GET  https://tigertag-cdn.web.app/__/firebase/init.json
    +
    +POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={apiKey}
    +     { "email": "user@example.com", "password": "…", "returnSecureToken": true }
    +
    +GET  https://firestore.googleapis.com/v1/projects/{projectId}/databases/(default)/documents/users/{uid}/inventory
    +     Authorization: Bearer {idToken}
    +

    {{how_code_note}}

    +
    +
    + +
    +

    {{int_title}}

    +

    {{int_lead}}

    +
    +
    + {{ha_pill}} +

    Home Assistant

    {{ha_text}}

    +
    {
    +  "domain": "tigertag",
    +  "name":   "TigerTag",
    +  "version": "0.1.0",
    +  "requirements": ["pyrebase4>=4.5.0"],
    +  "config_flow": true,
    +  "iot_class": "cloud_polling"
    +}
    + +
    +
    + {{esp_pill}} +

    ESP32 / Arduino

    {{esp_text}}

    +
    weight_available = max(0, measured_raw_weight - container_weight)
    + +
    + +
    + {{sm_pill}} +

    Spoolman bridge

    {{sm_text}}

    +
    pip install -r requirements.txt
    +export TIGERTAG_EMAIL=you@example.com
    +export TIGERTAG_PASSWORD=yourpassword
    +export SPOOLMAN_URL=http://192.168.1.10:7912
    +export DRY_RUN=1                    # dry-run mode
    +python3 sync.py
    + +
    +
    +
    + {{scale_pill}} +

    TigerScale

    {{scale_text}}

    + +
    +
    users/{uid}/scales/{mac}          ← heartbeat, every 30 s
    +users/{uid}/inventory/{spoolId}   ← weight_available + last_update
    +
    +
    +

    {{int_note}} CONTRIBUTING.md

    +
    + +
    +

    {{sec_title}}

    +

    {{sec_lead}}

    +
    +

    {{sec1_title}}

    {{sec1_text}}

    +

    {{sec2_title}}

    {{sec2_text}}

    + + +
    +
    + +
    +

    {{contract_title}}

    +

    {{contract_lead}}

    +
    +
    +

    {{stable_title}}

    +
      +
    • {{stable1}}
    • +
    • {{stable2}}
    • +
    • {{stable3}}
    • +
    • {{stable4}}
    • +
    +
    +
    +

    {{may_title}}

    +
      +
    • {{may1}}
    • +
    • {{may2}}
    • +
    • {{may3}}
    • +
    +
    +
    + +
    + +
    +

    {{docs_title}}

    +

    {{docs_lead}}

    + + +

    {{eco_title}}

    +

    {{eco_lead}}

    +
    +

    {{eco_proto_title}}

    {{eco_proto_text}}

    {{open}}
    +

    Tiger Studio Manager

    {{eco_studio_text}}

    {{repo}}
    + +
    +
    + +
    +

    {{faq_title}}

    +
    +
    {{faq1_q}}

    {{faq1_a}}

    +
    {{faq2_q}}

    {{faq2_a}}

    +
    {{faq3_q}}

    {{faq3_a}}

    +
    {{faq4_q}}

    {{faq4_a}}

    +
    {{faq5_q}}

    {{faq5_a}}

    +
    {{faq6_q}}

    {{faq6_a}}

    +
    {{faq7_q}}

    {{faq7_a}}

    +
    +
    + +
    +
    +

    {{join_title}}

    {{join_text}}

    + {{join_btn}} +
    +
    +
    + + + + From 823b5290a17f9713a7022d1bef5d99605d70fa61 Mon Sep 17 00:00:00 2001 From: BenGlut Date: Fri, 6 Nov 2026 08:54:16 +0100 Subject: [PATCH 2/2] TigerTag Corp is now TigerSystem The publisher and trademark holder is named TigerSystem in the copyright headers, the licences, the README and the showcase site (footer in every language and structured data). --- LICENSES/Apache-2.0.txt | 2 +- LICENSING.md | 4 ++-- README.md | 2 +- site/build.py | 4 ++-- site/i18n/de.json | 4 ++-- site/i18n/en.json | 4 ++-- site/i18n/es.json | 4 ++-- site/i18n/fr.json | 4 ++-- site/i18n/it.json | 4 ++-- site/i18n/nl.json | 4 ++-- site/i18n/pl.json | 4 ++-- site/i18n/pt-pt.json | 4 ++-- site/i18n/pt.json | 4 ++-- site/i18n/ru.json | 4 ++-- site/i18n/zh.json | 4 ++-- site/seo_check.py | 2 +- 16 files changed, 29 insertions(+), 29 deletions(-) diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt index 23810a1..051ebfc 100644 --- a/LICENSES/Apache-2.0.txt +++ b/LICENSES/Apache-2.0.txt @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 TigerTag Corp + Copyright 2025 TigerSystem Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSING.md b/LICENSING.md index decb426..1f665bc 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -47,8 +47,8 @@ notice. That is the only case where it matters. in the protocol repository. You may say your project is *"compatible with TigerTag"*. You may not put our logo on a product as a mark of authenticity without written authorization. - **The TigerTag service itself** — the Firebase project, the API and the catalogue are operated by - TigerTag Corp. This repository documents how to talk to them; it does not license them. -- **The TigerTag+ signature.** Anyone may *verify* a signature offline. Only TigerTag Corp may + TigerSystem. This repository documents how to talk to them; it does not license them. +- **The TigerTag+ signature.** Anyone may *verify* a signature offline. Only TigerSystem may *issue* one — we hold the private key. --- diff --git a/README.md b/README.md index 80f3906..c1d56d3 100644 --- a/README.md +++ b/README.md @@ -244,4 +244,4 @@ The **TigerTag protocol** is CC-BY-4.0 with an irrevocable, royalty-free right t see [TigerTag-RFID-Guide](https://github.com/TigerTag-Project/TigerTag-RFID-Guide). The TigerTag service itself (the Firebase project, the API and the catalogue) is operated by -TigerTag Corp and is not covered by these licences. +TigerSystem and is not covered by these licences. diff --git a/site/build.py b/site/build.py index 69e69cf..4328dc1 100644 --- a/site/build.py +++ b/site/build.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # # TigerTag Firebase Integration Guide -# Copyright (c) 2025-2026 TigerTag Corp. +# Copyright (c) 2025-2026 TigerSystem. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -118,7 +118,7 @@ def jsonld_blocks(lang, words, site_url, url, date): faq = [{"@type": "Question", "name": words[f"faq{i}_q"], "acceptedAnswer": {"@type": "Answer", "text": words[f"faq{i}_a"]}} for i in range(1, FAQ_COUNT + 1)] return [ - {"@context": "https://schema.org", "@type": "Organization", "@id": ORG_ID, "name": "TigerTag Corp", + {"@context": "https://schema.org", "@type": "Organization", "@id": ORG_ID, "name": "TigerSystem", "url": "https://tigersystem.io", "logo": site_url + "assets/apple-touch-icon.png", "sameAs": ["https://github.com/TigerTag-Project", "https://tigertag-project.github.io/TigerTag-RFID-Guide/"]}, diff --git a/site/i18n/de.json b/site/i18n/de.json index 01ed247..3c13d7c 100644 --- a/site/i18n/de.json +++ b/site/i18n/de.json @@ -116,8 +116,8 @@ "join_title": "Starten Sie Ihre Integration", "join_text": "Wählen Sie in den Schnelllinks des README den Leitfaden für Ihre Plattform und folgen Sie ihm von der Anmeldung bis zum ersten Lesezugriff.", "join_btn": "Schnelllinks öffnen", - "foot_licences": "Dokumentation unter CC-BY-4.0, Code und Ausschnitte unter Apache-2.0. Der TigerTag-Dienst wird von TigerTag Corp betrieben und fällt nicht unter diese Lizenzen.", - "foot_trademark": "TigerTag ist eine Marke der TigerTag Corp. Markenrichtlinie", + "foot_licences": "Dokumentation unter CC-BY-4.0, Code und Ausschnitte unter Apache-2.0. Der TigerTag-Dienst wird von TigerSystem betrieben und fällt nicht unter diese Lizenzen.", + "foot_trademark": "TigerTag ist eine Marke von TigerSystem. Markenrichtlinie", "foot_source": "Quelltext dieser Seite", "foot_issues": "Problem in der Dokumentation melden", "repo": "Repository", diff --git a/site/i18n/en.json b/site/i18n/en.json index 851ff36..1c7d57c 100644 --- a/site/i18n/en.json +++ b/site/i18n/en.json @@ -116,8 +116,8 @@ "join_title": "Start your integration", "join_text": "Pick the guide for your platform in the README's quick links, and follow it from sign-in to your first read.", "join_btn": "Open the quick links", - "foot_licences": "Documentation under CC-BY-4.0, code and snippets under Apache-2.0. The TigerTag service is operated by TigerTag Corp and is not covered by these licences.", - "foot_trademark": "TigerTag is a trademark of TigerTag Corp. Trademark policy", + "foot_licences": "Documentation under CC-BY-4.0, code and snippets under Apache-2.0. The TigerTag service is operated by TigerSystem and is not covered by these licences.", + "foot_trademark": "TigerTag is a trademark of TigerSystem. Trademark policy", "foot_source": "Source of this page", "foot_issues": "Report a documentation issue", "repo": "Repository", diff --git a/site/i18n/es.json b/site/i18n/es.json index b969e77..e5fcd46 100644 --- a/site/i18n/es.json +++ b/site/i18n/es.json @@ -116,8 +116,8 @@ "join_title": "Empieza tu integración", "join_text": "Elige la guía de tu plataforma en los enlaces rápidos del README y síguela desde el inicio de sesión hasta tu primera lectura.", "join_btn": "Abrir los enlaces rápidos", - "foot_licences": "Documentación bajo CC-BY-4.0, código y fragmentos bajo Apache-2.0. El servicio TigerTag lo opera TigerTag Corp y no está cubierto por estas licencias.", - "foot_trademark": "TigerTag es una marca de TigerTag Corp. Política de marca", + "foot_licences": "Documentación bajo CC-BY-4.0, código y fragmentos bajo Apache-2.0. El servicio TigerTag lo opera TigerSystem y no está cubierto por estas licencias.", + "foot_trademark": "TigerTag es una marca de TigerSystem. Política de marca", "foot_source": "Código fuente de esta página", "foot_issues": "Informar de un problema en la documentación", "repo": "Repositorio", diff --git a/site/i18n/fr.json b/site/i18n/fr.json index e8f4876..be4a3f9 100644 --- a/site/i18n/fr.json +++ b/site/i18n/fr.json @@ -116,8 +116,8 @@ "join_title": "Démarrez votre intégration", "join_text": "Choisissez le guide de votre plateforme dans les liens rapides du README, et suivez-le de la connexion jusqu'à votre première lecture.", "join_btn": "Ouvrir les liens rapides", - "foot_licences": "Documentation sous CC-BY-4.0, code et extraits sous Apache-2.0. Le service TigerTag est exploité par TigerTag Corp et n'est pas couvert par ces licences.", - "foot_trademark": "TigerTag est une marque de TigerTag Corp. Politique de marque", + "foot_licences": "Documentation sous CC-BY-4.0, code et extraits sous Apache-2.0. Le service TigerTag est exploité par TigerSystem et n'est pas couvert par ces licences.", + "foot_trademark": "TigerTag est une marque de TigerSystem. Politique de marque", "foot_source": "Source de cette page", "foot_issues": "Signaler un problème de documentation", "repo": "Dépôt", diff --git a/site/i18n/it.json b/site/i18n/it.json index 4584b50..bb71ffb 100644 --- a/site/i18n/it.json +++ b/site/i18n/it.json @@ -116,8 +116,8 @@ "join_title": "Inizia la tua integrazione", "join_text": "Scegli la guida per la tua piattaforma nei link rapidi del README e seguila dall'accesso alla tua prima lettura.", "join_btn": "Apri i link rapidi", - "foot_licences": "Documentazione sotto CC-BY-4.0, codice e frammenti sotto Apache-2.0. Il servizio TigerTag è gestito da TigerTag Corp e non è coperto da queste licenze.", - "foot_trademark": "TigerTag è un marchio di TigerTag Corp. Politica sul marchio", + "foot_licences": "Documentazione sotto CC-BY-4.0, codice e frammenti sotto Apache-2.0. Il servizio TigerTag è gestito da TigerSystem e non è coperto da queste licenze.", + "foot_trademark": "TigerTag è un marchio di TigerSystem. Politica sul marchio", "foot_source": "Sorgente di questa pagina", "foot_issues": "Segnala un problema nella documentazione", "repo": "Repository", diff --git a/site/i18n/nl.json b/site/i18n/nl.json index ebd26c7..487e1c3 100644 --- a/site/i18n/nl.json +++ b/site/i18n/nl.json @@ -116,8 +116,8 @@ "join_title": "Begin je integratie", "join_text": "Kies de handleiding voor jouw platform in de snelkoppelingen van de README, en volg die van het inloggen tot je eerste leesactie.", "join_btn": "Open de snelkoppelingen", - "foot_licences": "Documentatie onder CC-BY-4.0, code en fragmenten onder Apache-2.0. De dienst TigerTag wordt beheerd door TigerTag Corp en valt niet onder deze licenties.", - "foot_trademark": "TigerTag is een handelsmerk van TigerTag Corp. Merkenbeleid", + "foot_licences": "Documentatie onder CC-BY-4.0, code en fragmenten onder Apache-2.0. De dienst TigerTag wordt beheerd door TigerSystem en valt niet onder deze licenties.", + "foot_trademark": "TigerTag is een handelsmerk van TigerSystem. Merkenbeleid", "foot_source": "Bron van deze pagina", "foot_issues": "Meld een probleem met de documentatie", "repo": "Repository", diff --git a/site/i18n/pl.json b/site/i18n/pl.json index 5aa3fa1..67a34dc 100644 --- a/site/i18n/pl.json +++ b/site/i18n/pl.json @@ -116,8 +116,8 @@ "join_title": "Rozpocznij swoją integrację", "join_text": "Wybierz przewodnik dla swojej platformy w szybkich linkach README i przejdź przez niego od logowania do pierwszego odczytu.", "join_btn": "Otwórz szybkie linki", - "foot_licences": "Dokumentacja na licencji CC-BY-4.0, kod i fragmenty kodu na licencji Apache-2.0. Usługę TigerTag obsługuje TigerTag Corp i nie jest ona objęta tymi licencjami.", - "foot_trademark": "TigerTag jest znakiem towarowym TigerTag Corp. Polityka znaków towarowych", + "foot_licences": "Dokumentacja na licencji CC-BY-4.0, kod i fragmenty kodu na licencji Apache-2.0. Usługę TigerTag obsługuje TigerSystem i nie jest ona objęta tymi licencjami.", + "foot_trademark": "TigerTag jest znakiem towarowym TigerSystem. Polityka znaków towarowych", "foot_source": "Źródło tej strony", "foot_issues": "Zgłoś problem z dokumentacją", "repo": "Repozytorium", diff --git a/site/i18n/pt-pt.json b/site/i18n/pt-pt.json index dab6463..3eec3a0 100644 --- a/site/i18n/pt-pt.json +++ b/site/i18n/pt-pt.json @@ -116,8 +116,8 @@ "join_title": "Comece a sua integração", "join_text": "Escolha o guia para a sua plataforma nas ligações rápidas do README e siga-o desde o início de sessão até à primeira leitura.", "join_btn": "Abrir as ligações rápidas", - "foot_licences": "Documentação sob CC-BY-4.0, código e excertos sob Apache-2.0. O serviço TigerTag é operado pela TigerTag Corp e não está abrangido por estas licenças.", - "foot_trademark": "TigerTag é uma marca da TigerTag Corp. Política de marca", + "foot_licences": "Documentação sob CC-BY-4.0, código e excertos sob Apache-2.0. O serviço TigerTag é operado pela TigerSystem e não está abrangido por estas licenças.", + "foot_trademark": "TigerTag é uma marca da TigerSystem. Política de marca", "foot_source": "Código-fonte desta página", "foot_issues": "Comunicar um problema na documentação", "repo": "Repositório", diff --git a/site/i18n/pt.json b/site/i18n/pt.json index ac2fa81..b71987d 100644 --- a/site/i18n/pt.json +++ b/site/i18n/pt.json @@ -116,8 +116,8 @@ "join_title": "Comece sua integração", "join_text": "Escolha o guia da sua plataforma nos links rápidos do README e siga-o do login até a sua primeira leitura.", "join_btn": "Abrir os links rápidos", - "foot_licences": "Documentação sob CC-BY-4.0, código e trechos sob Apache-2.0. O serviço TigerTag é operado pela TigerTag Corp e não é coberto por essas licenças.", - "foot_trademark": "TigerTag é uma marca da TigerTag Corp. Política de marca", + "foot_licences": "Documentação sob CC-BY-4.0, código e trechos sob Apache-2.0. O serviço TigerTag é operado pela TigerSystem e não é coberto por essas licenças.", + "foot_trademark": "TigerTag é uma marca da TigerSystem. Política de marca", "foot_source": "Código-fonte desta página", "foot_issues": "Relatar um problema na documentação", "repo": "Repositório", diff --git a/site/i18n/ru.json b/site/i18n/ru.json index c6884ce..8602757 100644 --- a/site/i18n/ru.json +++ b/site/i18n/ru.json @@ -116,8 +116,8 @@ "join_title": "Начните свою интеграцию", "join_text": "Выберите руководство для своей платформы в быстрых ссылках README и следуйте ему от входа до первого чтения.", "join_btn": "Открыть быстрые ссылки", - "foot_licences": "Документация под лицензией CC-BY-4.0, код и фрагменты кода под лицензией Apache-2.0. Сервис TigerTag управляется TigerTag Corp и на эти лицензии не распространяется.", - "foot_trademark": "TigerTag — товарный знак TigerTag Corp. Политика использования товарного знака", + "foot_licences": "Документация под лицензией CC-BY-4.0, код и фрагменты кода под лицензией Apache-2.0. Сервис TigerTag управляется TigerSystem и на эти лицензии не распространяется.", + "foot_trademark": "TigerTag — товарный знак TigerSystem. Политика использования товарного знака", "foot_source": "Исходный код этой страницы", "foot_issues": "Сообщить о проблеме в документации", "repo": "Репозиторий", diff --git a/site/i18n/zh.json b/site/i18n/zh.json index 9f82a94..1a16f2e 100644 --- a/site/i18n/zh.json +++ b/site/i18n/zh.json @@ -116,8 +116,8 @@ "join_title": "开始您的集成", "join_text": "在 README 的快速链接中选择适合您平台的指南,从登录一直跟随到第一次读取。", "join_btn": "打开快速链接", - "foot_licences": "文档采用 CC-BY-4.0 许可,代码和代码片段采用 Apache-2.0 许可。TigerTag 服务由 TigerTag Corp 运营,不在这些许可范围内。", - "foot_trademark": "TigerTag 是 TigerTag Corp 的商标。商标政策", + "foot_licences": "文档采用 CC-BY-4.0 许可,代码和代码片段采用 Apache-2.0 许可。TigerTag 服务由 TigerSystem 运营,不在这些许可范围内。", + "foot_trademark": "TigerTag 是 TigerSystem 的商标。商标政策", "foot_source": "本页源代码", "foot_issues": "报告文档问题", "repo": "代码仓库", diff --git a/site/seo_check.py b/site/seo_check.py index 3752201..653a010 100644 --- a/site/seo_check.py +++ b/site/seo_check.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: Apache-2.0 # # TigerTag Firebase Integration Guide -# Copyright (c) 2025-2026 TigerTag Corp. +# Copyright (c) 2025-2026 TigerSystem. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.