feat(app): Noto Sans and Noto Sans JP, bundled, the same on every device

The app brought no fonts: text took each device's own, a minimal Linux
showed Japanese as boxes, and kanji could take Chinese shapes. Now Noto
Sans is the app's font everywhere (Latin, Greek with polytonic,
Cyrillic), and Noto Sans JP fills in kana and kanji in their Japanese
shapes. Noto Sans JP alone wouldn't do: it lacks accented and polytonic
Greek. Both come with the app and are never fetched; scripts neither
covers fall back to the device's own fonts.

Each family is one variable font holding every weight (Flutter sets the
weight axis from the requested weight; a test checks bold inks more):
0.7 MB Noto Sans, 0.7 MB its italic, 8.1 MB Noto Sans JP, from Noto's own
GitHub releases (assets/fonts/ATTRIBUTION.md). Their SIL Open Font
License texts join the app's licence list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-08 23:02:08 +02:00
commit 4345741646
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
11 changed files with 331 additions and 21 deletions

View file

@ -0,0 +1,14 @@
# Fonts
The app brings its fonts with it and never fetches any: text looks the same on
every device, whatever fonts it has.
| File | Font | Source | Licence |
|---|---|---|---|
| `NotoSans-Variable.ttf`, `NotoSans-Italic-Variable.ttf` | Noto Sans (Latin, Greek incl. polytonic, Cyrillic), every weight | [notofonts/latin-greek-cyrillic](https://github.com/notofonts/latin-greek-cyrillic) release NotoSans-v2.015, `unhinted/slim-variable-ttf` | SIL Open Font License 1.1, `OFL-NotoSans.txt` |
| `NotoSansJP-Variable.otf` | Noto Sans JP (kana, kanji in Japanese shapes), every weight | [notofonts/noto-cjk](https://github.com/notofonts/noto-cjk) `Sans/Variable/OTF/Subset/NotoSansJP-VF.otf`, commit f8d157532fbf (2026-10-08) | SIL Open Font License 1.1, `OFL-NotoSansJP.txt` |
Noto Sans is the app's font; Noto Sans JP fills in the glyphs Noto Sans lacks
(`lib/src/core/theme/app_fonts.dart`). Noto Sans JP alone would not do: it has
no accented or polytonic Greek. The files are renamed only; their glyphs are
unchanged. The licences are shown in the app's licence list.

View file

@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -3,12 +3,14 @@ import 'package:halkyon_learn_flutter/src/app/person_app.dart';
import 'package:halkyon_learn_flutter/src/core/bloc/app_bloc_observer.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies.dart';
import 'package:halkyon_learn_flutter/src/core/dependencies/dependencies_scope.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/widget/session_gate.dart';
import 'package:material_ui/material_ui.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
Bloc.observer = const AppBlocObserver();
registerAppFontLicenses();
final dependencies = await Dependencies.create();
runApp(
DependenciesScope(

View file

@ -0,0 +1,25 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
/// The app's font, the same on every device: Noto Sans, for Latin, Greek
/// (polytonic too) and Cyrillic. Bundled, never fetched (pubspec.yaml), so
/// text looks alike everywhere and needs nothing the device may lack.
const appFontFamily = 'Noto Sans';
/// What fills in where [appFontFamily] has no glyph: Noto Sans JP, for kana
/// and kanji in their Japanese shapes. Noto Sans JP alone can't be the app's
/// font: it lacks accented and polytonic Greek.
const appFontFallback = ['Noto Sans JP'];
/// Adds the bundled fonts' licences to the app's licence list, beside the
/// packages' own: the SIL Open Font License asks that it travels with them.
void registerAppFontLicenses() => LicenseRegistry.addLicense(() async* {
for (final (family, file) in const [
('Noto Sans', 'assets/fonts/OFL-NotoSans.txt'),
('Noto Sans JP', 'assets/fonts/OFL-NotoSansJP.txt'),
]) {
yield LicenseEntryWithLineBreaks([
family,
], await rootBundle.loadString(file));
}
});

View file

@ -1,11 +1,15 @@
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
import 'package:halkyon_learn_flutter/src/core/theme/brand.dart';
import 'package:halkyon_learn_flutter/src/core/theme/theme_extras.dart';
import 'package:material_ui/material_ui.dart';
/// The app's theme for [brightness]: Material 3 with a hand-authored colour
/// scheme, [ThemeExtras], and the current style of progress indicators.
/// scheme, the app's own fonts ([appFontFamily]), [ThemeExtras], and the
/// current style of progress indicators.
ThemeData appTheme(Brightness brightness) => ThemeData(
useMaterial3: true,
fontFamily: appFontFamily,
fontFamilyFallback: appFontFallback,
colorScheme: switch (brightness) {
Brightness.light => _lightScheme(),
Brightness.dark => _darkScheme(),

View file

@ -147,26 +147,22 @@ flutter:
- assets/icon/halkyon_icon.png
# Region maps for map questions (see assets/maps/ATTRIBUTION.md).
- assets/maps/japan_prefectures.geojson
# The bundled fonts' licences, for the licence list (see assets/fonts/ATTRIBUTION.md).
- assets/fonts/OFL-NotoSans.txt
- assets/fonts/OFL-NotoSansJP.txt
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
# The app's own fonts, the same on every device and never fetched: Noto Sans for
# Latin, Greek and Cyrillic, Noto Sans JP behind it for kana and kanji
# (core/theme/app_fonts.dart). Each file holds every weight (variable fonts).
fonts:
- family: Noto Sans
fonts:
- asset: assets/fonts/NotoSans-Variable.ttf
- asset: assets/fonts/NotoSans-Italic-Variable.ttf
style: italic
- family: Noto Sans JP
fonts:
- asset: assets/fonts/NotoSansJP-Variable.otf
# The app's icons, written by tool/app_icons.py: it prepares the images derived from
# the kingfisher in build/icons/ first, then runs `dart run flutter_launcher_icons`

View file

@ -0,0 +1,84 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_fonts.dart';
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
import 'package:material_ui/material_ui.dart';
/// Loads the bundled font [family] from [files], as the app does at start.
Future<void> load(String family, List<String> files) async {
final loader = FontLoader(family);
for (final file in files) {
loader.addFont(rootBundle.load(file));
}
await loader.load();
}
/// How many pixels [text] inks in [family] at [weight]: heavier strokes ink
/// more.
Future<int> ink(String text, String family, FontWeight weight) async {
final painter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
fontFamily: family,
fontSize: 40,
fontWeight: weight,
color: const Color(0xFF000000),
),
),
textDirection: TextDirection.ltr,
)..layout();
final recorder = ui.PictureRecorder();
painter.paint(Canvas(recorder), Offset.zero);
final image = await recorder.endRecording().toImage(
painter.width.ceil(),
painter.height.ceil(),
);
final pixels = (await image.toByteData())!;
var inked = 0;
for (var alpha = 3; alpha < pixels.lengthInBytes; alpha += 4) {
if (pixels.getUint8(alpha) > 128) {
inked++;
}
}
return inked;
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('the theme sets text in Noto Sans, with Noto Sans JP for what it '
'lacks', () {
for (final brightness in Brightness.values) {
final style = appTheme(brightness).textTheme.bodyLarge!;
expect(style.fontFamily, 'Noto Sans');
expect(style.fontFamilyFallback, ['Noto Sans JP']);
}
});
test('each bundled font draws every weight from its one file', () async {
await load('Noto Sans', ['assets/fonts/NotoSans-Variable.ttf']);
await load('Noto Sans JP', ['assets/fonts/NotoSansJP-Variable.otf']);
for (final (family, text) in [
('Noto Sans', 'Ἀθῆναι Halkyon'),
('Noto Sans JP', '日本語のかな'),
]) {
final regular = await ink(text, family, FontWeight.w400);
final bold = await ink(text, family, FontWeight.w700);
expect(bold, greaterThan(regular * 1.2), reason: family);
}
});
test("the fonts' licences join the licence list", () async {
registerAppFontLicenses();
final packages = {
await for (final entry in LicenseRegistry.licenses) ...entry.packages,
};
expect(packages, containsAll(['Noto Sans', 'Noto Sans JP']));
});
}